diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 5b486a8bb5..cf61f78b6d 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -18,7 +18,7 @@ permissions: jobs: benchmark: runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v6 with: @@ -29,10 +29,11 @@ jobs: with: python-version: "3.11" + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Install dependencies - run: | - pip install poetry - poetry install --with dev + run: uv sync --group dev - name: Install system dependencies run: | @@ -42,7 +43,7 @@ jobs: # Generate benchmark comparison report using our branch-based script - name: Generate benchmark comparison report run: | - poetry run python bbot/scripts/benchmark_report.py \ + uv run python bbot/scripts/benchmark_report.py \ --base ${{ github.base_ref }} \ --current ${{ github.head_ref }} \ --output benchmark_report.md \ @@ -51,7 +52,7 @@ jobs: # Upload benchmark results as artifacts - name: Upload benchmark results - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: benchmark-results path: | @@ -66,101 +67,85 @@ jobs: with: script: | const fs = require('fs'); - - try { - const report = fs.readFileSync('benchmark_report.md', 'utf8'); - - // Find existing benchmark comment (with pagination) + + // Helper: find existing benchmark comments on this PR + async function findBenchmarkComments() { const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - per_page: 100, // Get more comments per page + per_page: 100, }); - - // Debug: log all comments to see what we're working with + console.log(`Found ${comments.data.length} comments on this PR`); - comments.data.forEach((comment, index) => { - console.log(`Comment ${index}: user=${comment.user.login}, body preview="${comment.body.substring(0, 100)}..."`); - }); - - const existingComments = comments.data.filter(comment => + + const benchmarkComments = comments.data.filter(comment => comment.body.toLowerCase().includes('performance benchmark') && comment.user.login === 'github-actions[bot]' ); - - console.log(`Found ${existingComments.length} existing benchmark comments`); - - if (existingComments.length > 0) { - // Sort comments by creation date to find the most recent - const sortedComments = existingComments.sort((a, b) => + + console.log(`Found ${benchmarkComments.length} existing benchmark comments`); + return benchmarkComments; + } + + // Helper: post or update the benchmark comment + async function upsertComment(body) { + const existing = await findBenchmarkComments(); + + if (existing.length > 0) { + const sorted = existing.sort((a, b) => new Date(b.created_at) - new Date(a.created_at) ); - - const mostRecentComment = sortedComments[0]; - console.log(`Updating most recent benchmark comment: ${mostRecentComment.id} (created: ${mostRecentComment.created_at})`); - + await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, - comment_id: mostRecentComment.id, - body: report + comment_id: sorted[0].id, + body: body }); - console.log('Updated existing benchmark comment'); - - // Delete any older duplicate comments - if (existingComments.length > 1) { - console.log(`Deleting ${existingComments.length - 1} older duplicate comments`); - for (let i = 1; i < sortedComments.length; i++) { - const commentToDelete = sortedComments[i]; - console.log(`Attempting to delete comment ${commentToDelete.id} (created: ${commentToDelete.created_at})`); - - try { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: commentToDelete.id - }); - console.log(`Successfully deleted duplicate comment: ${commentToDelete.id}`); - } catch (error) { - console.error(`Failed to delete comment ${commentToDelete.id}: ${error.message}`); - console.error(`Error details:`, error); - } + console.log(`Updated benchmark comment: ${sorted[0].id}`); + + // Clean up older duplicates + for (let i = 1; i < sorted.length; i++) { + try { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: sorted[i].id + }); + console.log(`Deleted duplicate comment: ${sorted[i].id}`); + } catch (e) { + console.error(`Failed to delete comment ${sorted[i].id}: ${e.message}`); } } } else { - // Create new comment await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - body: report + body: body }); console.log('Created new benchmark comment'); } - } catch (error) { - console.error('Failed to post benchmark results:', error); - - // Post a fallback comment - const fallbackMessage = [ - '## Performance Benchmark Report', - '', - '> ⚠️ **Failed to generate detailed benchmark comparison**', - '> ', - '> The benchmark comparison failed to run. This might be because:', - '> - Benchmark tests don\'t exist on the base branch yet', - '> - Dependencies are missing', - '> - Test execution failed', - '> ', - '> Please check the [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.', - '> ', - '> 📁 Benchmark artifacts may be available for download from the workflow run.' - ].join('\\n'); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: fallbackMessage - }); - } \ No newline at end of file + } + + let report; + try { + report = fs.readFileSync('benchmark_report.md', 'utf8'); + } catch (e) { + console.error('Failed to read benchmark report:', e.message); + report = `## Performance Benchmark Report + + > **Failed to generate detailed benchmark comparison** + > + > The benchmark comparison failed to run. This might be because: + > - Benchmark tests don't exist on the base branch yet + > - Dependencies are missing + > - Test execution failed + > + > Please check the [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. + > + > Benchmark artifacts may be available for download from the workflow run.`; + } + + await upsertComment(report); diff --git a/.github/workflows/distro_tests.yml b/.github/workflows/distro_tests.yml index 72b73dcacc..6dfd07de1c 100644 --- a/.github/workflows/distro_tests.yml +++ b/.github/workflows/distro_tests.yml @@ -17,12 +17,20 @@ jobs: os: ["ubuntu:22.04", "ubuntu:24.04", "debian", "archlinux", "fedora", "kalilinux/kali-rolling", "parrotsec/security"] steps: - uses: actions/checkout@v6 - - name: Install Python and Poetry + - name: Install Python and uv run: | if [ -f /etc/os-release ]; then . /etc/os-release if [ "$ID" = "ubuntu" ] || [ "$ID" = "debian" ] || [ "$ID" = "kali" ] || [ "$ID" = "parrotsec" ]; then export DEBIAN_FRONTEND=noninteractive + # Pin Parrot to MIT mirror to avoid stale third-party mirrors (e.g. mirror.clarkson.edu) + # Note: parrotsec/security image reports ID=debian, so we detect via parrot.list + if [ -f /etc/apt/sources.list.d/parrot.list ]; then + rm -f /etc/apt/sources.list /etc/apt/sources.list.old /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources + echo "deb https://mirrors.mit.edu/parrot/ echo main contrib non-free non-free-firmware" > /etc/apt/sources.list.d/parrot.list + echo "deb https://mirrors.mit.edu/parrot/ echo-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list.d/parrot.list + echo "deb https://mirrors.mit.edu/parrot/ echo-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list.d/parrot.list + fi apt-get update apt-get -y install curl git bash build-essential docker.io libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev elif [ "$ID" = "alpine" ]; then @@ -51,22 +59,25 @@ jobs: pyenv rehash python3.11 -m pip install --user pipx python3.11 -m pipx ensurepath - pipx install poetry + pipx install uv " - name: Set OS Environment Variable run: echo "OS_NAME=${{ matrix.os }}" | sed 's|[:/]|_|g' >> $GITHUB_ENV - name: Run tests run: | - export PATH="$HOME/.local/bin:$PATH" - export PATH="$HOME/.pyenv/bin:$PATH" - export PATH="$HOME/.pyenv/shims:$PATH" + # Preserve pyenv/pipx paths installed under the GitHub-set HOME + export PATH="/github/home/.local/bin:$PATH" + export PATH="/github/home/.pyenv/bin:$PATH" + export PATH="/github/home/.pyenv/shims:$PATH" + # Fix HOME to match euid (root) so rustup/cargo don't refuse to build + export HOME="$(getent passwd $(whoami) | cut -d: -f6)" export BBOT_DISTRO_TESTS=true - poetry env use python3.11 - poetry install - poetry run pytest --reruns 2 --exitfirst -o timeout_func_only=true --timeout 1200 --disable-warnings --log-cli-level=INFO . + uv python pin 3.11 + uv sync --group dev + uv run pytest --reruns 2 --exitfirst -o timeout_func_only=true --timeout 1200 --disable-warnings --log-cli-level=INFO . - name: Upload Debug Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: pytest-debug-logs-${{ env.OS_NAME }} path: pytest_debug.log diff --git a/.github/workflows/docs_updater.yml b/.github/workflows/docs_updater.yml index 94d5222aca..e645461660 100644 --- a/.github/workflows/docs_updater.yml +++ b/.github/workflows/docs_updater.yml @@ -17,13 +17,13 @@ jobs: uses: actions/setup-python@v6 with: python-version: "3.x" + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Install dependencies - run: | - pip install poetry - poetry install + run: uv sync --group dev - name: Generate docs run: | - poetry run bbot/scripts/docs.py + uv run bbot/scripts/docs.py - name: Create or Update Pull Request uses: peter-evans/create-pull-request@v8 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 40ccbb1ea1..8cddad737e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: # if one python version fails, let the others finish fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 - name: Set up Python @@ -26,20 +26,22 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set Python Version Environment Variable run: echo "PYTHON_VERSION=${{ matrix.python-version }}" | sed 's|[:/]|_|g' >> $GITHUB_ENV + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Install dependencies - run: | - pip install poetry - poetry install + run: uv sync --group dev - name: Lint run: | - poetry run ruff check - poetry run ruff format --check + uv run ruff check + uv run ruff format --check - name: Run tests + env: + BBOT_IO_API_KEY: ${{ secrets.BBOT_IO_API_KEY }} run: | - poetry run pytest -vv --reruns 2 -o timeout_func_only=true --timeout 1200 --disable-warnings --log-cli-level=INFO --cov-config=bbot/test/coverage.cfg --cov-report xml:cov.xml --cov=bbot . + uv run pytest -vv --reruns 2 -o timeout_func_only=true --timeout 1200 --disable-warnings --log-cli-level=INFO --cov-config=bbot/test/coverage.cfg --cov-report xml:cov.xml --cov=bbot . - name: Upload Debug Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: pytest-debug-logs-${{ env.PYTHON_VERSION }} path: pytest_debug.log @@ -69,14 +71,32 @@ jobs: uses: actions/setup-python@v6 with: python-version: "3.x" - - name: Install dependencies + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Calculate version + id: calc_version run: | - python -m pip install --upgrade pip - pip install poetry build - poetry self add "poetry-dynamic-versioning[plugin]" + # Get base version from latest stable tag (exclude rc tags, strip 'v' prefix) + LATEST_STABLE_TAG=$(git describe --tags --abbrev=0 --exclude="*rc*") + BASE_VERSION=$(echo "$LATEST_STABLE_TAG" | sed 's/^v//') + + if [[ "${{ github.ref }}" == "refs/heads/stable" ]]; then + # Stable: clean version from tag + VERSION="$BASE_VERSION" + elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then + # Dev: version.distancerc (e.g., 3.0.0.123rc) + DISTANCE=$(git rev-list ${LATEST_STABLE_TAG}..HEAD --count) + VERSION="${BASE_VERSION}.${DISTANCE}rc" + fi + + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "Calculated version: $VERSION" + + # Write version to file for hatchling to pick up + echo "__version__ = \"$VERSION\"" > bbot/_version.py - name: Build Pypi package if: github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/dev' - run: python -m build + run: uv build - name: Publish Pypi package if: github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/dev' uses: pypa/gh-action-pypi-publish@release/v1.13 @@ -85,7 +105,7 @@ jobs: - name: Get BBOT version id: version run: | - FULL_VERSION=$(poetry version | cut -d' ' -f2) + FULL_VERSION="${{ steps.calc_version.outputs.VERSION }}" echo "BBOT_VERSION=$FULL_VERSION" >> $GITHUB_OUTPUT # Extract major.minor (e.g., 2.7 from 2.7.1) MAJOR_MINOR=$(echo "$FULL_VERSION" | cut -d'.' -f1-2) @@ -100,7 +120,6 @@ jobs: push: true context: . tags: | - blacklanternsecurity/bbot:latest blacklanternsecurity/bbot:dev blacklanternsecurity/bbot:${{ steps.version.outputs.BBOT_VERSION }} blacklanternsecurity/bbot:${{ steps.version.outputs.BBOT_VERSION_MAJOR_MINOR }} @@ -112,6 +131,7 @@ jobs: push: true context: . tags: | + blacklanternsecurity/bbot:latest blacklanternsecurity/bbot:stable blacklanternsecurity/bbot:${{ steps.version.outputs.BBOT_VERSION }} blacklanternsecurity/bbot:${{ steps.version.outputs.BBOT_VERSION_MAJOR_MINOR }} @@ -124,7 +144,6 @@ jobs: file: Dockerfile.full context: . tags: | - blacklanternsecurity/bbot:latest-full blacklanternsecurity/bbot:dev-full blacklanternsecurity/bbot:${{ steps.version.outputs.BBOT_VERSION }}-full blacklanternsecurity/bbot:${{ steps.version.outputs.BBOT_VERSION_MAJOR_MINOR }}-full @@ -137,6 +156,7 @@ jobs: file: Dockerfile.full context: . tags: | + blacklanternsecurity/bbot:latest-full blacklanternsecurity/bbot:stable-full blacklanternsecurity/bbot:${{ steps.version.outputs.BBOT_VERSION }}-full blacklanternsecurity/bbot:${{ steps.version.outputs.BBOT_VERSION_MAJOR_MINOR }}-full @@ -177,6 +197,28 @@ jobs: done outputs: BBOT_VERSION: ${{ steps.version.outputs.BBOT_VERSION }} + tag_commit: + needs: publish_code + runs-on: ubuntu-latest + if: github.event_name == 'push' && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/dev') + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Configure git + run: | + git config --local user.email "info@blacklanternsecurity.com" + git config --local user.name "GitHub Actions" + - name: Tag commit + run: | + VERSION="v${{ needs.publish_code.outputs.BBOT_VERSION }}" + if [[ "${{ github.ref }}" == "refs/heads/stable" ]]; then + git tag -a "$VERSION" -m "Stable Release $VERSION" + else + git tag -a "$VERSION" -m "Dev Release $VERSION" + fi + git push origin "$VERSION" + publish_docs: runs-on: ubuntu-latest if: github.event_name == 'push' && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/dev') @@ -194,10 +236,10 @@ jobs: path: .cache restore-keys: | mkdocs-material- + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Install dependencies - run: | - pip install poetry - poetry install --only=docs + run: uv sync --only-group docs - name: Configure Git run: | git config user.name github-actions @@ -211,35 +253,12 @@ jobs: - name: Generate docs (stable branch) if: github.ref == 'refs/heads/stable' run: | - poetry run mike deploy Stable + uv run mike deploy Stable - name: Generate docs (dev branch) if: github.ref == 'refs/heads/dev' run: | - poetry run mike deploy Dev + uv run mike deploy Dev - name: Publish docs run: | git switch gh-pages git push - # tag_commit: - # needs: publish_code - # runs-on: ubuntu-latest - # if: github.event_name == 'push' && github.ref == 'refs/heads/stable' - # steps: - # - uses: actions/checkout@v6 - # with: - # ref: ${{ github.head_ref }} - # fetch-depth: 0 # Fetch all history for all tags and branches - # - name: Configure git - # run: | - # git config --local user.email "info@blacklanternsecurity.com" - # git config --local user.name "GitHub Actions" - # - name: Tag commit - # run: | - # VERSION="${{ needs.publish_code.outputs.BBOT_VERSION }}" - # if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then - # TAG_MESSAGE="Dev Release $VERSION" - # elif [[ "${{ github.ref }}" == "refs/heads/stable" ]]; then - # TAG_MESSAGE="Stable Release $VERSION" - # fi - # git tag -a $VERSION -m "$TAG_MESSAGE" - # git push origin --tags diff --git a/.gitignore b/.gitignore index 0c6b86a341..768643121b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ .coverage* /data/ /neo4j/ +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..88e017d24f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,950 @@ +# BBOT Developer Guide + +## Core Principles + +### Modularity Principle +When writing a BBOT module, make sure all module-specific code lives in the module itself. Don't hard-code module-specific things in core or in helpers. + +### DRY Principle +Don't Repeat Yourself -- and interpret this broadly. If two pieces of code aren't identical but follow a similar enough pattern that they could be generalized, they should be. Extract shared logic into a common abstraction rather than duplicating the pattern. Usually this means creating a shared helper, or a shared module template in `bbot/modules/templates`. When you notice structural similarity, unify it. + +### Engineering Principle +Every system that is implemented must be implemented properly. No hacks, no hardcoding, no shortcuts. If we implement one of something, we build a proper system for it. It's okay to take a step back from the current task, in order to do things right. This relates directly to the Modularity Principle above. + +### Testing Principle +BBOT has extremely thorough tests, including **one or more individual tests for each module, with no exceptions**. This is critical to maintaining stability in a recursive tool, which by its nature flirts with race conditions and infinite loops. If you add a module, you write a test. If you change a module, you make sure its test still passes. + +--- + +## Tooling + +- **Package manager**: [uv](https://docs.astral.sh/uv/) +- **Linter/formatter**: [ruff](https://docs.astral.sh/ruff/) (pinned to 0.15.2) +- **Test framework**: [pytest](https://docs.pytest.org/) with pytest-asyncio +- **Python**: 3.10 - 3.14 + +--- + +## Dev Environment Setup + +```bash +# 1. Fork and clone +git clone git@github.com:/bbot.git +cd bbot + +# 2. Switch to dev branch, then create a feature branch +git checkout dev +git checkout -b my-feature + +# 3. Install uv (if you haven't already) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# 4. Install all dependencies (including dev) +uv sync --group dev + +# 5. Install pre-commit hooks (ruff, file checks, etc.) +uv run pre-commit install + +# 6. Activate the virtualenv +source .venv/bin/activate + +# 7. Verify +bbot --help +``` + +### Running Tests + +```bash +# Run the full suite +./bbot/test/run_tests.sh + +# Run specific module tests +./bbot/test/run_tests.sh robots,sslcert + +# Run a single test file directly +pytest bbot/test/test_step_2/module_tests/test_module_robots.py -x -vv +``` + +### Linting + +```bash +ruff check # lint +ruff format # auto-format +ruff format --check # verify formatting without changes +``` + +### Git Workflow + +- `stable` - production releases +- `dev` - active development, PR target +- Feature branches are created from `dev` + +--- + +## Architecture Overview + +### How a Scan Works + +BBOT is an async, recursive OSINT tool. A scan starts with **seed events** (targets) and passes them through a pipeline of **modules**. Each module watches for specific event types, processes them, and may emit new events, which feed back into the pipeline. This continues until no module has anything left to do. + +``` + Seeds (targets) + | + v + +--------------+ + | ScanIngress | dedup, blacklist, scope check + +--------------+ + | + v + +--------------+ + | Intercept | dns, cloud tagging + | Modules | (modify/tag events before distribution) + +--------------+ + | + v + +--------------+ + | ScanEgress | scope filtering, graph management + +--------------+ + | + v + +-----------+-----------+ + | | | + Module A Module B Module C ... + | | | + +-----------+-----------+ + | + v + Output Modules (json, csv, neo4j, ...) +``` + +### Events + +Events are the currency of BBOT. Every piece of data -- a hostname, IP, URL, open port, finding -- is an event. Events have: + +- **type**: `DNS_NAME`, `IP_ADDRESS`, `URL`, `OPEN_TCP_PORT`, `HTTP_RESPONSE`, `FINDING`, `VULNERABILITY`, `EMAIL_ADDRESS`, etc. +- **data**: the actual data (a string, dict, etc.) +- **parent**: the event that led to this one (forming a discovery chain) +- **scope_distance**: how many hops from the original target (0 = in-scope) +- **tags**: metadata like `in-scope`, `affiliate`, `cloud-azure`, `open-port`, etc. +- **module**: which module discovered it + +### Scope Distance + +Scope distance tracks how far an event is from the original target: +- `0` = explicitly in-scope (matches target or discovered in-scope) +- `1` = one hop away (e.g. a hostname found in an SSL cert of an in-scope host) +- `2+` = further away + +The scan's `scope.search_distance` (default 0) controls how far modules are allowed to look. A module's `scope_distance_modifier` adjusts this per-module. + +### Helpers + +BBOT has a helper for almost everything. **Please use them.** They're accessible via `self.helpers` inside any module. + +Key helpers: + +| Helper | What it does | +|--------|-------------| +| `self.helpers.request(url)` | Make an HTTP request (with retries, SSL handling, etc.) | +| `self.helpers.resolve(host)` | DNS resolution | +| `self.helpers.is_ip(s)` | Check if string is an IP | +| `self.helpers.is_dns_name(s)` | Check if string is a hostname | +| `self.helpers.split_domain(host)` | Split into subdomain + root domain | +| `self.helpers.domain_parents(domain)` | Get all parent domains | +| `self.helpers.make_netloc(host, port)` | Format `host:port` (handles IPv6) | +| `self.helpers.parent_domain(domain)` | Get immediate parent domain | +| `self.helpers.beautifulsoup(html, parser)` | Parse HTML | +| `self.helpers.validators.validate_host(h)` | Validate and normalize a hostname | +| `self.helpers.tempfile(data, pipe=False)` | Create a temp file with content | +| `self.helpers.run(command)` | Run a shell command | +| `self.helpers.run_live(command)` | Run a shell command, stream output | +| `self.helpers.as_completed(tasks)` | Async iteration of completed tasks | +| `self.helpers.wordlist(url_or_path)` | Download/cache a wordlist | +| `self.helpers.rand_string(n)` | Random string of length n | +| `self.helpers.regexes.email_regex` | Pre-compiled email regex | +| `self.helpers.add_get_params(url, params)` | Add query params to a URL | +| `self.helpers.quote(s)` | URL-encode a string | +| `self.helpers.make_ip_type(s)` | Convert string to `ipaddress` object | +| `self.helpers.parse_port_string(s)` | Parse port range string (e.g. `"80,443,8000-9000"`) | +| `self.helpers.top_tcp_ports(n)` | Get top N TCP ports | + +There are hundreds more in `bbot/core/helpers/misc.py`. Browse them before writing utility code yourself. + +--- + +## Writing a Module + +### Quick Start + +1. Create `bbot/modules/my_module.py` +2. Define a class that inherits from `BaseModule` +3. Set `watched_events`, `produced_events`, `flags`, and `meta` +4. Implement `handle_event()` +5. Create `bbot/test/test_step_2/module_tests/test_module_my_module.py` + +Here's a minimal module: + +```python +from bbot.modules.base import BaseModule + + +class my_module(BaseModule): + watched_events = ["DNS_NAME"] + produced_events = ["EMAIL_ADDRESS"] + flags = ["passive", "email-enum"] + meta = { + "description": "Query example.com for email addresses", + "created_date": "2025-01-01", + "author": "@you", + } + + async def handle_event(self, event): + url = f"https://api.example.com/lookup?domain={event.data}" + r = await self.helpers.request(url) + if r and r.status_code == 200: + for email in r.json().get("emails", []): + await self.emit_event( + email, + "EMAIL_ADDRESS", + parent=event, + context=f"{{module}} queried example.com and found {{event.type}}: {{event.data}}", + ) +``` + +And its test: + +```python +from .base import ModuleTestBase + + +class TestMyModule(ModuleTestBase): + async def setup_after_prep(self, module_test): + module_test.httpx_mock.add_response( + url="https://api.example.com/lookup?domain=blacklanternsecurity.com", + json={"emails": ["info@blacklanternsecurity.com"]}, + ) + + def check(self, module_test, events): + assert any( + e.data == "info@blacklanternsecurity.com" and e.type == "EMAIL_ADDRESS" + for e in events + ), "Failed to find email" +``` + +### Module Lifecycle + +``` +setup() --> handle_event() (called many times) --> finish() --> report() --> cleanup() +``` + +1. **`setup()`** - one-time initialization (validate config, download data, check API keys) +2. **`handle_event(event)`** - called for each matching event +3. **`finish()`** - called when the scan is finishing; can still emit events +4. **`report()`** - called once after finish; for summary output +5. **`cleanup()`** - called last; close files, delete temp data; **cannot** emit events + +--- + +### Module Attributes Reference + +#### Event Configuration + +##### `watched_events` (list) +Event types this module wants to process. The module's `handle_event()` is only called for these types. + +```python +# sslcert.py - watches for open ports to grab SSL certs from +watched_events = ["OPEN_TCP_PORT"] + +# newsletters.py - watches HTTP responses to scan HTML +watched_events = ["HTTP_RESPONSE"] + +# json.py (output module) - watches everything +watched_events = ["*"] +``` + +##### `produced_events` (list) +Event types this module may emit. Used for dependency resolution and documentation. + +```python +# sslcert.py - can discover hostnames and emails from certificates +produced_events = ["DNS_NAME", "EMAIL_ADDRESS"] + +# portscan.py - finds open ports +produced_events = ["OPEN_TCP_PORT"] +``` + +##### `flags` (list) +Tags that describe the module's behavior. Must include at least one activity flag (`passive` or `active`). Must also include `safe`, `loud`, or `invasive` (or a combination of `loud` and `invasive`). + +Common flags: +- `passive` / `active` - whether the module touches the target directly +- `safe` - non-intrusive and non-destructive +- `loud` - generates a large amount of network traffic +- `invasive` - intrusive or potentially destructive +- `subdomain-enum` - participates in subdomain enumeration +- `web` - basic web scanning +- `email-enum` - email discovery + +```python +# crt.py - queries a third-party API, never touches the target +flags = ["subdomain-enum", "passive"] + +# sslcert.py - connects directly to target ports +flags = ["affiliates", "subdomain-enum", "email-enum", "active", "web"] +``` + +##### `meta` (dict) +Module metadata. Must include `description`, `created_date`, and `author`. Set `auth_required: True` if the module needs an API key. + +```python +meta = { + "description": "Query crt.sh (certificate transparency) for subdomains", + "created_date": "2022-05-13", + "author": "@TheTechromancer", +} + +# For API-key modules: +meta = {"description": "Query API for subdomains", "auth_required": True} +``` + +--- + +#### Options + +##### `options` / `options_desc` (dict) +User-configurable settings. Access them via `self.config.get("option_name")`. + +```python +# robots.py - configurable parsing options +options = {"include_sitemap": False, "include_allow": True, "include_disallow": True} +options_desc = { + "include_sitemap": "Include 'sitemap' entries", + "include_allow": "Include 'Allow' Entries", + "include_disallow": "Include 'Disallow' Entries", +} + +# In handle_event(): +if self.config.get("include_sitemap") is True: + ... +``` + +```python +# sslcert.py - timeout and behavior options +options = {"timeout": 5.0, "skip_non_ssl": True} +options_desc = {"timeout": "Socket connect timeout in seconds", "skip_non_ssl": "Don't try common non-SSL ports"} +``` + +--- + +#### Scope & Filtering + +##### `scope_distance_modifier` (int or None) -- default: `0` +Controls which events the module accepts based on how far they are from the target. + +- `0` (default) - accept events up to the scan's configured search distance +- `1` - accept events up to search distance + 1 +- `None` - accept all events regardless of distance + +```python +# sslcert.py - looks one hop beyond normal scope, because certificate names +# found on in-scope hosts often reveal related infrastructure +scope_distance_modifier = 1 +``` + +##### `in_scope_only` (bool) -- default: `False` +Only accept events that are explicitly in-scope (distance == 0). More restrictive than `scope_distance_modifier = 0`. + +```python +# robots.py - only fetch robots.txt for in-scope hosts +in_scope_only = True +``` + +##### `target_only` (bool) -- default: `False` +Only accept the initial target/seed events. Useful for modules that should only run once against the original targets. + +##### `accept_seeds` (bool) -- default: `True` for passive, `False` for active +Whether to process seed events (the initial targets provided to the scan). + +##### `accept_url_special` (bool) -- default: `False` +Whether to accept "special" URLs (e.g. JavaScript files) that are not normally distributed to web modules. + +```python +# httpx.py - needs to process all URLs including special ones +accept_url_special = True +``` + +--- + +#### Deduplication + +##### `accept_dupes` (bool) -- default: `False` +Whether to accept the same event more than once. Most modules should leave this `False`. + +```python +# Output modules set this True because they need to see every event +accept_dupes = True +``` + +##### `suppress_dupes` (bool) -- default: `True` +Whether to suppress duplicate *outgoing* events. Prevents the same event from being emitted twice. + +##### `per_host_only` (bool) -- default: `False` +Only process one event per unique host. After processing `1.2.3.4`, skip any future events for `1.2.3.4`. + +##### `per_hostport_only` (bool) -- default: `False` +Only process one event per unique host:port combination. + +```python +# robots.py - only fetch robots.txt once per host:port +per_hostport_only = True +``` + +##### `per_domain_only` (bool) -- default: `False` +Only process one event per unique root domain. After processing `www.example.com`, skip `api.example.com`. + +```python +# emailformat.py - one API query per domain is enough +per_domain_only = True +``` + +##### `_incoming_dedup_hash(self, event)` -- override for custom dedup +Override this to define custom deduplication logic. Return a hash (int) or `(hash, reason_string)`. + +```python +# securitytxt.py - dedupe by parent domain so we only check security.txt once +# per parent domain, not once per subdomain +def _incoming_dedup_hash(self, event): + parent_domain = self.helpers.parent_domain(event.data) + return hash(parent_domain), "already processed parent domain" +``` + +```python +# subdomain_enum.py template - dedupe by highest or lowest parent domain +def _incoming_dedup_hash(self, event): + return hash(self.make_query(event)), f"dedup_strategy={self.dedup_strategy}" +``` + +--- + +#### Concurrency & Batching + +##### `_module_threads` (int) -- default: `1` +How many `handle_event()` calls can run concurrently. Increase this for I/O-bound modules. + +```python +# sslcert.py - connects to many hosts in parallel +_module_threads = 25 +``` + +##### `_batch_size` (int) -- default: `1` +When > 1, events are collected into batches and passed to `handle_batch(*events)` instead of `handle_event()`. Useful for tools that work better with bulk input. + +```python +# portscan.py - masscan is most efficient with all targets at once +batch_size = 1000000 + +async def handle_batch(self, *events): + targets, correlator = await self.make_targets(events, self.syn_scanned) + async for ip, port, parent_event in self.masscan(targets, correlator): + await self.emit_open_port(ip, port, parent_event) +``` + +##### `_shuffle_incoming_queue` (bool) -- default: `True` +Whether to randomize the order of incoming events. Set to `False` when order matters. + +```python +# portscan.py - processes all events together, order doesn't matter but +# we disable shuffle because batch_size is huge +_shuffle_incoming_queue = False +``` + +--- + +#### Dependencies + +##### `deps_pip` (list) +Python packages to install. + +```python +# sslcert.py +deps_pip = ["pyOpenSSL~=25.3.0"] +``` + +##### `deps_apt` (list) +System packages to install. + +```python +# sslcert.py +deps_apt = ["openssl"] +``` + +##### `deps_modules` (list) +Other BBOT modules that must be enabled for this module to work. + +##### `deps_shell` (list) +Shell commands to run for installation (uses Ansible's `shell` module). + +##### `deps_ansible` (list) +Ansible tasks for complex dependency installation (downloading binaries, etc.). + +```python +# fingerprintx.py - downloads a Go binary +deps_ansible = [ + { + "name": "Download fingerprintx", + "unarchive": { + "src": "https://github.com/.../fingerprintx_{version}_{platform}_{arch}.tar.gz", + "include": "fingerprintx", + "dest": "#{BBOT_TOOLS}", + "remote_src": True, + }, + }, +] +``` + +--- + +#### Priority & Queue + +##### `_priority` (int) -- default: `3` +Module priority from 1 (highest) to 5 (lowest). Lower-priority modules get events first. + +```python +# sslcert.py - runs early because other modules depend on the hostnames it discovers +_priority = 2 +``` + +##### `_qsize` (int) -- default: `1000` +Outgoing event queue size. A smaller queue creates backpressure that helps with rate limiting. + +```python +# subdomain_enum.py template - small queue to combat API rate limiting +_qsize = 10 +``` + +##### `_preserve_graph` (bool) -- default: `False` +Accept duplicate events that are needed for complete event chain construction. Only used by output modules. + +```python +# json.py - needs complete event chains for accurate output +_preserve_graph = True +``` + +##### `_stats_exclude` (bool) -- default: `False` +Exclude this module from scan statistics. Used by output and report modules. + +##### `_disable_auto_module_deps` (bool) -- default: `False` +Prevent BBOT from automatically enabling dependency modules. For example, if your module watches `URL` events, BBOT normally auto-enables `httpx`. Set this to `True` to prevent that. + +--- + +### Key Methods + +#### `handle_event(self, event)` -- the core method + +Called once for each matching event. This is where your module does its work. + +```python +# robots.py - fetch and parse robots.txt +async def handle_event(self, event): + host = f"{event.parsed_url.scheme}://{event.parsed_url.netloc}/" + url = f"{host}robots.txt" + result = await self.helpers.request(url) + if result: + body = result.text + if body: + for line in body.split("\n"): + if line.startswith("Disallow:"): + path = line.split(": ", 1)[1].lstrip("/") + await self.emit_event( + f"{host}{path}", + "URL_UNVERIFIED", + parent=event, + tags=["spider-danger"], + ) +``` + +#### `handle_batch(self, *events)` -- bulk processing + +Used when `_batch_size > 1`. Receives multiple events at once. + +```python +# portscan.py - bulk port scanning with masscan +async def handle_batch(self, *events): + targets, correlator = await self.make_targets(events, self.syn_scanned) + async for ip, port, parent_event in self.masscan(targets, correlator): + await self.emit_open_port(ip, port, parent_event) +``` + +#### `filter_event(self, event)` -- custom event filtering + +Called before `handle_event()`. Return `True` to accept, `False` to reject, or `(False, "reason")` to reject with a logged reason. + +```python +# sslcert.py - skip ports that don't typically use SSL +async def filter_event(self, event): + if self.skip_non_ssl and event.port in self.non_ssl_ports: + return False, f"Port {event.port} doesn't typically use SSL" + return True +``` + +```python +# subdomain_enum.py template - reject wildcards and cloud resources +async def filter_event(self, event): + query = self.make_query(event) + is_wildcard = await self._is_wildcard(query) + if self.reject_wildcards and is_wildcard: + return False, "Event is a wildcard domain" + return True, "" +``` + +#### `setup(self)` -- one-time initialization + +Return values: +- `True` -- success +- `(True, "message")` -- success with message +- `None` or `(None, "message")` -- **soft fail**: module is disabled, scan continues +- `False` or `(False, "message")` -- **hard fail**: scan aborts + +```python +# portscan.py - validates config, checks masscan, checks IPv6 support +async def setup(self): + self.top_ports = self.config.get("top_ports", 100) + self.rate = self.config.get("rate", 300) + self.ports = self.config.get("ports", "") + if self.ports: + try: + self.helpers.parse_port_string(self.ports) + except ValueError as e: + return False, f"Error parsing ports '{self.ports}': {e}" + # ... + return True +``` + +```python +# subdomain_enum_apikey template - soft-fail if API key is missing +async def setup(self): + await super().setup() + return await self.require_api_key() + # Returns (None, "No API key set") if missing, disabling the module +``` + +#### `finish(self)` -- called when scan is finishing + +Can still emit events. May be called multiple times if new activity is detected. + +#### `report(self)` -- summary output + +Called once after `finish()`. Use for generating tables or summary data. + +```python +# asn.py - output ASN statistics +async def report(self): + self.log_table(table_data, headers=["ASN", "Subnet", "Count"], table_name="asns") +``` + +#### `cleanup(self)` -- resource cleanup + +Called once at the very end. Close files, delete temp files. **Cannot emit events.** + +```python +# json.py +async def cleanup(self): + if getattr(self, "_file", None) is not None: + with suppress(Exception): + self.file.close() +``` + +```python +# portscan.py +async def cleanup(self): + with suppress(Exception): + self.exclude_file.unlink() +``` + +--- + +### Emitting Events + +#### `emit_event(data, event_type, parent, **kwargs)` + +Creates and queues an event for processing by other modules. + +```python +# Simple string event +await self.emit_event("sub.example.com", "DNS_NAME", parent=event) + +# With context (used for discovery chain documentation) +await self.emit_event( + "sub.example.com", + "DNS_NAME", + parent=event, + context=f"{{module}} queried crt.sh and found {{event.type}}: {{event.data}}", +) + +# With tags +await self.emit_event(url, "URL_UNVERIFIED", parent=event, tags=["spider-danger"]) + +# FINDING event (dict data) +await self.emit_event( + { + "host": str(event.host), + "description": "Found something interesting", + "url": event.data["url"], + "severity": "HIGH", + }, + "FINDING", + parent=event, +) +``` + +#### `make_event(data, event_type, parent, **kwargs)` + +Creates an event without emitting it. Useful when you need to inspect or modify it first. + +```python +ssl_event = self.make_event(hostname, "DNS_NAME", parent=event, raise_error=True) +if ssl_event: + await self.emit_event(ssl_event, tags=["affiliate"]) +``` + +--- + +### API Helpers + +#### `api_request(url, **kwargs)` + +HTTP request with automatic retry, rate-limit handling (429), API key cycling, and failure tracking. After too many failures, the module enters error state. + +```python +r = await self.api_request("https://api.example.com/search?q=test") +if r and r.status_code == 200: + data = r.json() +``` + +#### `require_api_key()` + +Validates that an API key is configured. Call in `setup()`. + +```python +async def setup(self): + return await self.require_api_key() +``` + +#### `api_page_iter(url, page_size=100, **kwargs)` + +Async generator for paginated API results. URL can contain `{page}`, `{page_size}`, and `{offset}` placeholders. + +```python +async for page in self.api_page_iter( + "https://api.example.com/search?q=test&page={page}&limit={page_size}" +): + if not page.get("results"): + break + for result in page["results"]: + await self.emit_event(result["hostname"], "DNS_NAME", parent=event) +``` + +--- + +### Running External Processes + +```python +# Run a command and get the result +result = await self.run_process(["nmap", "-p", "22,80", target]) +if result.returncode == 0: + output = result.stdout + +# Stream output line-by-line (for long-running tools) +async for line in self.run_process_live(["masscan", "-oJ", "-", ...]): + data = json.loads(line) +``` + +--- + +### Logging + +```python +self.debug("Low-level detail") # only visible with -d flag +self.verbose("Useful but not critical") # visible with -v flag +self.info("Standard info") +self.success("Something good happened") # green +self.warning("Something concerning") # orange +self.error("Something failed") # red + +# "Huge" variants: entire line in bold color +self.hugesuccess("Major discovery!") +self.hugewarning("Major concern!") +``` + +--- + +### Templates + +For common patterns, inherit from a template instead of `BaseModule` directly. Templates live in `bbot/modules/templates/`: + +- **`subdomain_enum`** - passive subdomain enumeration via free API. Handles dedup, wildcard rejection, query building. +- **`subdomain_enum_apikey`** - same as above but requires an API key. +- **`shodan`** - Shodan API integration. +- **`github`** - GitHub API integration. +- **`censys`** - Censys API integration. +- **`bucket`** - Cloud storage bucket enumeration. +- **`webhook`** - Webhook output. + +Example: `crt.py` inherits from `subdomain_enum` and only needs to override the request/parse logic: + +```python +from bbot.modules.templates.subdomain_enum import subdomain_enum + + +class crt(subdomain_enum): + flags = ["subdomain-enum", "passive"] + watched_events = ["DNS_NAME"] + produced_events = ["DNS_NAME"] + meta = { + "description": "Query crt.sh (certificate transparency) for subdomains", + "created_date": "2022-05-13", + "author": "@TheTechromancer", + } + base_url = "https://crt.sh" + + async def request_url(self, query): + params = {"q": f"%.{query}", "output": "json"} + url = self.helpers.add_get_params(self.base_url, params).geturl() + return await self.api_request(url, timeout=self.http_timeout + 30) + + async def parse_results(self, r, query): + results = set() + for cert_info in r.json(): + domain = cert_info.get("name_value") + if domain: + for d in domain.splitlines(): + results.add(d.lower()) + return results +``` + +--- + +### Module Types + +#### Scan Modules (default) +Normal modules that watch events and produce new ones. This is what you'll write 95% of the time. + +#### Output Modules +Inherit from `BaseOutputModule`. Receive all events and write them somewhere. + +```python +from bbot.modules.output.base import BaseOutputModule + +class my_output(BaseOutputModule): + watched_events = ["*"] + meta = {"description": "Custom output"} + _preserve_graph = True # maintain complete event chains + + async def handle_event(self, event): + # write event to file, database, API, etc. + ... +``` + +Output modules automatically get: +- `accept_dupes = True` +- `scope_distance_modifier = None` (see all events) +- `_stats_exclude = True` + +#### Internal Modules +Inherit from `BaseInternalModule`. System-level modules that aren't exposed to users. + +#### Intercept Modules +Inherit from `BaseInterceptModule`. Special high-priority modules that can modify or reject events before they reach normal modules. Used for DNS resolution, cloud detection, etc. You probably don't need to write one. + +--- + +### Writing Tests + +Every module needs a test in `bbot/test/test_step_2/module_tests/`. The test file must be named `test_module_.py`. + +Test classes inherit from `ModuleTestBase` and follow this pattern: + +```python +from .base import ModuleTestBase + + +class TestMyModule(ModuleTestBase): + # Optional: override targets (default: ["blacklanternsecurity.com"]) + targets = ["http://127.0.0.1:8888"] + + # Optional: override which modules are enabled + modules_overrides = ["httpx", "my_module"] + + # Optional: override config + config_overrides = {"modules": {"my_module": {"some_option": True}}} + + async def setup_before_prep(self, module_test): + """Called BEFORE the scan is prepared. Set up HTTP mocks here.""" + pass + + async def setup_after_prep(self, module_test): + """Called AFTER the scan is prepared. Modify modules, add mocks here.""" + # Mock an HTTP response + module_test.httpx_mock.add_response( + url="https://api.example.com/lookup?domain=blacklanternsecurity.com", + json={"results": ["sub.blacklanternsecurity.com"]}, + ) + + # Mock DNS + await module_test.mock_dns({ + "blacklanternsecurity.com": {"A": ["127.0.0.88"]}, + }) + + # Mock an HTTP server response + module_test.set_expect_requests( + expect_args={"method": "GET", "uri": "/robots.txt"}, + respond_args={"response_data": "Disallow: /secret/"}, + ) + + def check(self, module_test, events): + """Verify the scan produced the expected events.""" + assert any( + e.data == "sub.blacklanternsecurity.com" and e.type == "DNS_NAME" + for e in events + ), "Failed to find subdomain" +``` + +The test lifecycle runs: +1. `setup_before_prep()` - set up mocks +2. Scan `_prep()` - loads modules, config +3. `setup_after_prep()` - modify scan state +4. Scan runs and collects events +5. `check()` - your assertions + +### Test Utilities + +- **`module_test.httpx_mock`** - mock HTTP responses (from pytest-httpx) +- **`module_test.httpserver`** - real HTTP server on port 8888 +- **`module_test.httpserver_ssl`** - real HTTPS server on port 9999 +- **`module_test.mock_dns(data)`** - mock DNS responses +- **`module_test.mock_interactsh(name)`** - mock out-of-band interactions +- **`module_test.module`** - reference to the module instance being tested +- **`module_test.scan`** - reference to the Scanner instance + +Real example -- `test_module_robots.py`: + +```python +class TestRobots(ModuleTestBase): + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["httpx", "robots"] + config_overrides = {"modules": {"robots": {"include_sitemap": True}}} + + async def setup_after_prep(self, module_test): + robots = "Allow: /allow/\nDisallow: /disallow/\nSitemap: http://127.0.0.1:8888/sitemap.txt" + module_test.set_expect_requests( + expect_args={"method": "GET", "uri": "/robots.txt"}, + respond_args={"response_data": robots}, + ) + + def check(self, module_test, events): + assert any(e.data == "http://127.0.0.1:8888/allow/" for e in events) + assert any(e.data == "http://127.0.0.1:8888/disallow/" for e in events) + assert any(e.data == "http://127.0.0.1:8888/sitemap.txt" for e in events) +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..ec23a777d5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +Treat @AGENTS.md the same way you'd treat CLAUDE.md. \ No newline at end of file diff --git a/README.md b/README.md index 58f5425db0..0151868a5e 100644 --- a/README.md +++ b/README.md @@ -144,16 +144,16 @@ output_modules: ```bash # run a light web scan against www.evilcorp.com -bbot -t www.evilcorp.com -p web-basic +bbot -t www.evilcorp.com -p web # run a heavy web scan against www.evilcorp.com -bbot -t www.evilcorp.com -p web-thorough +bbot -t www.evilcorp.com -p web-heavy ``` - +
-web-basic.yml +web.yml ```yaml description: Quick web scan @@ -162,43 +162,43 @@ include: - iis-shortnames flags: - - web-basic + - web ```
- + - +
-web-thorough.yml +web-heavy.yml ```yaml description: Aggressive web scan include: - # include the web-basic preset - - web-basic + # include the web preset + - web flags: - - web-thorough + - web-heavy ```
- + ### 5) Everything Everywhere All at Once ```bash # everything everywhere all at once -bbot -t evilcorp.com -p kitchen-sink --allow-deadly +bbot -t evilcorp.com -p kitchen-sink # roughly equivalent to: -bbot -t evilcorp.com -p subdomain-enum cloud-enum code-enum email-enum spider web-basic paramminer dirbust-light web-screenshots --allow-deadly +bbot -t evilcorp.com -p subdomain-enum cloud-enum code-enum email-enum spider web paramminer dirbust-light web-screenshots ``` @@ -215,16 +215,11 @@ include: - code-enum - email-enum - spider - - web-basic + - web - paramminer - dirbust-light - web-screenshots - - baddns-intense - -config: - modules: - baddns: - enable_references: True + - baddns-heavy ``` diff --git a/bbot/__init__.py b/bbot/__init__.py index 914c45ff4b..7a87e25d8c 100644 --- a/bbot/__init__.py +++ b/bbot/__init__.py @@ -1,6 +1,4 @@ -# version placeholder (replaced by poetry-dynamic-versioning) -__version__ = "v0.0.0" - -from .scanner import Scanner, Preset - -__all__ = ["Scanner", "Preset"] +try: + from bbot._version import __version__ +except ImportError: + __version__ = "0.0.0" diff --git a/bbot/_version.py b/bbot/_version.py new file mode 100644 index 0000000000..6c8e6b979c --- /dev/null +++ b/bbot/_version.py @@ -0,0 +1 @@ +__version__ = "0.0.0" diff --git a/bbot/cli.py b/bbot/cli.py index 6f88447e1a..1985b2a9eb 100755 --- a/bbot/cli.py +++ b/bbot/cli.py @@ -7,7 +7,7 @@ from bbot.errors import * from bbot import __version__ from bbot.logger import log_to_stderr -from bbot.core.helpers.misc import chain_lists, rm_rf +from bbot.core.helpers.misc import chain_lists if multiprocessing.current_process().name == "MainProcess": @@ -56,6 +56,10 @@ async def _main(): return # ensure arguments (-c config options etc.) are valid options = preset.args.parsed + # apply CLI log level options (e.g. --debug/--verbose/--silent) to the + # global core logger even for CLI-only commands (like --install-all-deps) + # that don't construct a full Scanner. + preset.apply_log_level(apply_core=True) # print help if no arguments if len(sys.argv) == 1: @@ -90,7 +94,8 @@ async def _main(): preset._default_output_modules = options.output_modules preset._default_internal_modules = [] - preset.bake() + # Bake a temporary copy of the preset so that flags correctly enable their associated modules before listing them + preset = preset.bake() # --list-modules if options.list_modules: @@ -144,59 +149,57 @@ async def _main(): print(row) return - try: - scan = Scanner(preset=preset) - except (PresetAbortError, ValidationError) as e: - log.warning(str(e)) - return - - deadly_modules = [ - m for m in scan.preset.scan_modules if "deadly" in preset.preloaded_module(m).get("flags", []) - ] - if deadly_modules and not options.allow_deadly: - log.hugewarning(f"You enabled the following deadly modules: {','.join(deadly_modules)}") - log.hugewarning("Deadly modules are highly intrusive") - log.hugewarning("Please specify --allow-deadly to continue") - return False - - # --current-preset - if options.current_preset: - print(scan.preset.to_yaml()) + baked_preset = preset.bake() + + # --current-preset / --current-preset-full + if options.current_preset or options.current_preset_full: + # Ensure we always have a human-friendly description. Prefer an + # explicit scan_name if present, otherwise fall back to the + # preset name (e.g. "bbot_cli_main"). + if not baked_preset.description: + if baked_preset.scan_name: + baked_preset.description = str(baked_preset.scan_name) + elif baked_preset.name: + baked_preset.description = str(baked_preset.name) + if options.current_preset_full: + print(baked_preset.to_yaml(full_config=True)) + else: + print(baked_preset.to_yaml()) sys.exit(0) return - # --current-preset-full - if options.current_preset_full: - print(scan.preset.to_yaml(full_config=True)) - sys.exit(0) + try: + scan = Scanner(preset=baked_preset) + except (PresetAbortError, ValidationError) as e: + log.warning(str(e)) return # --install-all-deps if options.install_all_deps: + # create a throwaway Scanner solely so that Preset.bake(scan) can perform find_and_replace() on all module configs so that placeholders like "#{BBOT_TOOLS}" are resolved before running Ansible tasks. + from bbot.scanner import Scanner as _ScannerForDeps + preloaded_modules = preset.module_loader.preloaded() - scan_modules = [k for k, v in preloaded_modules.items() if str(v.get("type", "")) == "scan"] - output_modules = [k for k, v in preloaded_modules.items() if str(v.get("type", "")) == "output"] - log.verbose("Creating dummy scan with all modules + output modules for deps installation") - dummy_scan = Scanner(preset=preset, modules=scan_modules, output_modules=output_modules) - dummy_scan.helpers.depsinstaller.force_deps = True + modules_for_deps = [ + k for k, v in preloaded_modules.items() if str(v.get("type", "")) in ("scan", "output") + ] + + # dummy scan used only for environment preparation + dummy_scan = _ScannerForDeps(preset=preset) + + helper = dummy_scan.helpers log.info("Installing module dependencies") - await dummy_scan.load_modules() - log.verbose("Running module setups") - succeeded, hard_failed, soft_failed = await dummy_scan.setup_modules(deps_only=True) - # remove any leftovers from the dummy scan - rm_rf(dummy_scan.home, ignore_errors=True) - rm_rf(dummy_scan.temp_dir, ignore_errors=True) + succeeded, failed = await helper.depsinstaller.install(*modules_for_deps) if succeeded: log.success( f"Successfully installed dependencies for {len(succeeded):,} modules: {','.join(succeeded)}" ) - if soft_failed or hard_failed: - failed = soft_failed + hard_failed + if failed: log.warning(f"Failed to install dependencies for {len(failed):,} modules: {', '.join(failed)}") return False return True - scan_name = str(scan.name) + await scan._prep() log.verbose("") log.verbose("### MODULES ENABLED ###") @@ -205,12 +208,19 @@ async def _main(): log.verbose(row) scan.helpers.word_cloud.load() - await scan._prep() + + scan_name = str(scan.name) if not options.dry_run: log.trace(f"Command: {' '.join(sys.argv)}") - if sys.stdin.isatty(): + # In some environments (e.g. tests) stdin may be closed or not support isatty(). Treat those cases as non-interactive. + try: + stdin_is_tty = sys.stdin.isatty() + except (ValueError, io.UnsupportedOperation): + stdin_is_tty = False + + if stdin_is_tty: # warn if any targets belong directly to a cloud provider if not scan.preset.strict_scope: for event in scan.target.seeds.event_seeds: @@ -221,6 +231,24 @@ async def _main(): f'YOUR TARGET CONTAINS A CLOUD DOMAIN: "{event.host}". You\'re in for a wild ride!' ) + # warn about loud/invasive modules + loud_modules = [] + invasive_modules = [] + for m in scan.preset.scan_modules: + flags = scan.preset.preloaded_module(m).get("flags", []) + if "loud" in flags: + loud_modules.append(m) + if "invasive" in flags: + invasive_modules.append(m) + if loud_modules: + log.hugewarning( + f"LOUD modules enabled: {','.join(loud_modules)}. These generate a lot of traffic. To exclude, use -ef loud" + ) + if invasive_modules: + log.hugewarning( + f"INVASIVE modules enabled: {','.join(invasive_modules)}. These may be intrusive or destructive. To exclude, use -ef invasive" + ) + if not options.yes: log.hugesuccess(f"Scan ready. Press enter to execute {scan.name}") input() diff --git a/bbot/constants.py b/bbot/constants.py new file mode 100644 index 0000000000..364aead2ff --- /dev/null +++ b/bbot/constants.py @@ -0,0 +1,72 @@ +SCAN_STATUS_QUEUED = 0 +SCAN_STATUS_NOT_STARTED = 1 +SCAN_STATUS_STARTING = 2 +SCAN_STATUS_RUNNING = 3 +SCAN_STATUS_FINISHING = 4 +SCAN_STATUS_ABORTING = 5 +SCAN_STATUS_FINISHED = 6 +SCAN_STATUS_FAILED = 7 +SCAN_STATUS_ABORTED = 8 + + +SCAN_STATUSES = { + "QUEUED": SCAN_STATUS_QUEUED, + "NOT_STARTED": SCAN_STATUS_NOT_STARTED, + "STARTING": SCAN_STATUS_STARTING, + "RUNNING": SCAN_STATUS_RUNNING, + "FINISHING": SCAN_STATUS_FINISHING, + "ABORTING": SCAN_STATUS_ABORTING, + "FINISHED": SCAN_STATUS_FINISHED, + "FAILED": SCAN_STATUS_FAILED, + "ABORTED": SCAN_STATUS_ABORTED, +} + +SCAN_STATUS_CODES = {v: k for k, v in SCAN_STATUSES.items()} + + +def is_valid_scan_status(status): + """ + Check if a status is a valid scan status + """ + return status in SCAN_STATUSES + + +def is_valid_scan_status_code(status): + """ + Check if a status is a valid scan status code + """ + return status in SCAN_STATUS_CODES + + +def get_scan_status_name(status): + """ + Convert a numeric scan status code to a string status name + """ + try: + if isinstance(status, str): + if not is_valid_scan_status(status): + raise ValueError(f"Invalid scan status: {status}") + return status + elif isinstance(status, int): + return SCAN_STATUS_CODES[status] + else: + raise ValueError(f"Invalid scan status: {status} (must be int or str)") + except KeyError: + raise ValueError(f"Invalid scan status: {status}") + + +def get_scan_status_code(status): + """ + Convert a scan status string to a numeric status code + """ + try: + if isinstance(status, int): + if not is_valid_scan_status_code(status): + raise ValueError(f"Invalid scan status code: {status}") + return status + elif isinstance(status, str): + return SCAN_STATUSES[status] + else: + raise ValueError(f"Invalid scan status: {status} (must be int or str)") + except KeyError: + raise ValueError(f"Invalid scan status: {status}") diff --git a/bbot/core/config/logger.py b/bbot/core/config/logger.py index c5773a3a0c..dda240a8f6 100644 --- a/bbot/core/config/logger.py +++ b/bbot/core/config/logger.py @@ -78,6 +78,11 @@ def __init__(self, core): self.log_level = logging.INFO def cleanup_logging(self): + # Stop queue listener first (drains queue and stops monitor thread) + if self.listener is not None: + with suppress(Exception): + self.listener.stop() + # Close the queue handler with suppress(Exception): self.queue_handler.close() @@ -91,10 +96,6 @@ def cleanup_logging(self): with suppress(Exception): handler.close() - # Stop queue listener - with suppress(Exception): - self.listener.stop() - def setup_queue_handler(self, logging_queue=None, log_level=logging.DEBUG): if logging_queue is None: logging_queue = self.queue diff --git a/bbot/core/engine.py b/bbot/core/engine.py index d7c821a333..7a33f0da71 100644 --- a/bbot/core/engine.py +++ b/bbot/core/engine.py @@ -343,6 +343,26 @@ async def shutdown(self): self.context.term() except Exception: print(traceback.format_exc(), file=sys.stderr) + # terminate the server process/thread + if self._server_process is not None: + try: + self._server_process.join(timeout=5) + if self._server_process.is_alive(): + # threads don't have terminate/kill, only processes do + terminate = getattr(self._server_process, "terminate", None) + if callable(terminate): + terminate() + self._server_process.join(timeout=3) + if self._server_process.is_alive(): + kill = getattr(self._server_process, "kill", None) + if callable(kill): + kill() + except Exception: + with suppress(Exception): + kill = getattr(self._server_process, "kill", None) + if callable(kill): + kill() + self._server_process = None # delete socket file on exit self.socket_path.unlink(missing_ok=True) diff --git a/bbot/core/event/base.py b/bbot/core/event/base.py index 00a03aeed7..7f3f80fd79 100644 --- a/bbot/core/event/base.py +++ b/bbot/core/event/base.py @@ -1,5 +1,6 @@ import io import re +import sys import uuid import json import base64 @@ -11,6 +12,7 @@ from pathlib import Path from typing import Optional +from zoneinfo import ZoneInfo from copy import copy, deepcopy from contextlib import suppress from radixtarget import RadixTarget @@ -21,6 +23,7 @@ from bbot.errors import * from .helpers import EventSeed from bbot.core.helpers import ( + bytes_to_human, extract_words, is_domain, is_subdomain, @@ -40,6 +43,7 @@ validators, get_file_extension, ) +from bbot.models.helpers import utc_datetime_validator from bbot.core.helpers.web.envelopes import BaseEnvelope @@ -96,7 +100,8 @@ class BaseEvent: "timestamp": 1688526222.723366, "resolved_hosts": ["185.199.108.153"], "parent": "OPEN_TCP_PORT:cf7e6a937b161217eaed99f0c566eae045d094c7", - "tags": ["in-scope", "distance-0", "dir", "ip-185-199-108-153", "status-301", "http-title-301-moved-permanently"], + "tags": ["in-scope", "distance-0", "dir", "status-301"], + "http_title": "301 Moved Permanently", "module": "httpx", "module_sequence": "httpx" } @@ -106,9 +111,10 @@ class BaseEvent: # Always emit this event type even if it's not in scope _always_emit = False # Always emit events with these tags even if they're not in scope - _always_emit_tags = ["affiliate", "target"] + + _always_emit_tags = ["affiliate", "seed"] # Bypass scope checking and dns resolution, distribute immediately to modules - # This is useful for "end-of-line" events like FINDING and VULNERABILITY + # This is useful for "end-of-line" events like FINDING _quick_emit = False # Data validation, if data is a dictionary _data_validator = None @@ -119,6 +125,10 @@ class BaseEvent: # Don't allow duplicates to occur within a parent chain # In other words, don't emit the event if the same one already exists in its discovery context _suppress_chain_dupes = False + # Shared compiled regex for discovery context formatting (class-level to avoid per-instance overhead) + _discovery_context_regex = re.compile(r"\{(?:event|module)[^}]*\}") + # Stats class for the status line — override in subclasses for custom formatting + _stats_class = None # using __slots__ dramatically reduces memory usage in large scans __slots__ = [ @@ -147,23 +157,18 @@ class BaseEvent: "_graph_important", "_resolved_hosts", "_discovery_context", - "_discovery_context_regex", "_stats_recorded", "_internal", - "_confidence", "_dummy", "_module", # DNS-related attributes "dns_children", "raw_dns_records", "dns_resolve_distance", - # Web-related attributes - "web_spider_distance", - "parsed_url", - "url_extension", - "num_redirects", - # File-related attributes - "_data_path", + # Host metadata (cloud providers, ASN, whois, etc.) + "_host_metadata", + # Memory management + "_module_consumers", # Public attributes "module", "scan", @@ -179,7 +184,6 @@ def __init__( module=None, scan=None, tags=None, - confidence=100, timestamp=None, _dummy=False, _internal=None, @@ -197,7 +201,6 @@ def __init__( module (str, optional): Module that discovered the event. Defaults to None. scan (Scan, optional): BBOT Scan object. Required unless _dummy is True. Defaults to None. tags (list of str, optional): Descriptive tags for the event. Defaults to None. - confidence (int, optional): Confidence level for the event, on a scale of 1-100. Defaults to 100. timestamp (datetime, optional): Time of event discovery. Defaults to current UTC time. _dummy (bool, optional): If True, disables certain data validations. Defaults to False. _internal (Any, optional): If specified, makes the event internal. Defaults to None. @@ -226,8 +229,7 @@ def __init__( self.dns_children = {} self.raw_dns_records = {} self._discovery_context = "" - self._discovery_context_regex = re.compile(r"\{(?:event|module)[^}]*\}") - self.web_spider_distance = 0 + self._module_consumers = 0 # for creating one-off events without enforcing parent requirement self._dummy = _dummy @@ -245,7 +247,6 @@ def __init__( except AttributeError: self.timestamp = datetime.datetime.utcnow() - self.confidence = int(confidence) self._internal = False # self.scan holds the instantiated scan object (for helpers, etc.) @@ -285,27 +286,6 @@ def __init__( def data(self): return self._data - @property - def confidence(self): - return self._confidence - - @confidence.setter - def confidence(self, confidence): - self._confidence = min(100, max(1, int(confidence))) - - @property - def cumulative_confidence(self): - """ - Considers the confidence of parent events. This is useful for filtering out speculative/unreliable events. - - E.g. an event with a confidence of 50 whose parent is also 50 would have a cumulative confidence of 25. - - A confidence of 100 will reset the cumulative confidence to 100. - """ - if self._confidence == 100 or self.parent is None or self.parent is self: - return self._confidence - return int(self._confidence * self.parent.cumulative_confidence / 100) - @property def resolved_hosts(self): if is_ip(self.host): @@ -323,6 +303,18 @@ def data(self, data): self._port = None self._data = data + @property + def host_metadata(self): + try: + return self._host_metadata + except AttributeError: + self._host_metadata = {} + return self._host_metadata + + @host_metadata.setter + def host_metadata(self, value): + self._host_metadata = value + @property def internal(self): return self._internal @@ -383,6 +375,13 @@ def host_original(self): return self.host return self._host_original + @property + def url(self): + parsed_url = getattr(self, "parsed_url", None) + if parsed_url is not None: + return parsed_url.geturl() + return "" + @property def host_filterable(self): """ @@ -401,6 +400,8 @@ def host_filterable(self): @property def port(self): self.host + if self._port: + return self._port if getattr(self, "parsed_url", None): if self.parsed_url.port is not None: return self.parsed_url.port @@ -408,7 +409,6 @@ def port(self): return 443 elif self.parsed_url.scheme == "http": return 80 - return self._port @property def netloc(self): @@ -485,7 +485,7 @@ def tags(self, tags): self.add_tag(tag) def add_tag(self, tag): - self._tags.add(tagify(tag)) + self._tags.add(sys.intern(tagify(tag))) def add_tags(self, tags): for tag in set(tags): @@ -493,7 +493,7 @@ def add_tags(self, tags): def remove_tag(self, tag): with suppress(KeyError): - self._tags.remove(tagify(tag)) + self._tags.remove(sys.intern(tagify(tag))) @property def always_emit(self): @@ -618,6 +618,9 @@ def parent(self, parent): new_scope_distance += 1 self.scope_distance = new_scope_distance # inherit certain tags + # inherit seed tag from DNS_NAME_UNRESOLVED -> DNS_NAME only + if "seed" in parent.tags and parent.type == "DNS_NAME_UNRESOLVED" and self.type == "DNS_NAME": + self.add_tag("seed") if hosts_are_same: # inherit web spider distance from parent self.web_spider_distance = getattr(parent, "web_spider_distance", 0) @@ -692,6 +695,15 @@ def get_parents(self, omit=False, include_self=False): e = parent return parents + def _minimize(self): + """ + Called when a module is done processing this event. + + Decrements the consumer count. When no modules are left waiting to + process this event, heavy payload data is stripped to free memory. + """ + self._module_consumers = max(0, self._module_consumers - 1) + def clone(self): # Create a shallow copy of the event first cloned_event = copy(self) @@ -813,11 +825,11 @@ def __contains__(self, other): return True # hostnames and IPs radixtarget = RadixTarget() - radixtarget.insert(self.host) - return bool(radixtarget.search(other_event.host)) + radixtarget.insert(str(self.host)) + return bool(radixtarget.search(str(other_event.host))) return False - def json(self, mode="json", siem_friendly=False): + def json(self, mode="json"): """ Serializes the event object to a JSON-compatible dictionary. @@ -826,7 +838,6 @@ def json(self, mode="json", siem_friendly=False): Parameters: mode (str): Specifies the data serialization mode. Default is "json". Other options include "graph", "human", and "id". - siem_friendly (bool): Whether to format the JSON in a way that's friendly to SIEM ingestion by Elastic, Splunk, etc. This ensures the value of "data" is always the same type (a dictionary). Returns: dict: JSON-serializable dictionary representation of the event object. @@ -843,10 +854,12 @@ def json(self, mode="json", siem_friendly=False): data = data_attr else: data = smart_decode(self.data) - if siem_friendly: - j["data"] = {self.type: data} - else: + if isinstance(data, str): j["data"] = data + elif isinstance(data, dict): + j["data_json"] = data + else: + raise ValueError(f"Invalid data type: {type(data)}") # host, dns children if self.host: j["host"] = str(self.host) @@ -864,7 +877,7 @@ def json(self, mode="json", siem_friendly=False): if self.scan: j["scan"] = self.scan.id # timestamp - j["timestamp"] = self.timestamp.isoformat() + j["timestamp"] = utc_datetime_validator(self.timestamp).timestamp() # parent event parent_id = self.parent_id if parent_id: @@ -873,8 +886,7 @@ def json(self, mode="json", siem_friendly=False): if parent_uuid: j["parent_uuid"] = parent_uuid # tags - if self.tags: - j.update({"tags": list(self.tags)}) + j.update({"tags": sorted(self.tags)}) # parent module if self.module: j.update({"module": str(self.module)}) @@ -886,6 +898,10 @@ def json(self, mode="json", siem_friendly=False): j["discovery_path"] = self.discovery_path j["parent_chain"] = self.parent_chain + # host metadata (cloud providers, ASN, etc.) + host_metadata = getattr(self, "host_metadata", None) + if host_metadata: + j["host_metadata"] = host_metadata # parameter envelopes parameter_envelopes = getattr(self, "envelopes", None) if parameter_envelopes is not None: @@ -1073,13 +1089,13 @@ def _host(self): return make_ip_type(self.data["host"]) else: parsed = getattr(self, "parsed_url", None) - if parsed is not None: + if parsed is not None and parsed.hostname: return make_ip_type(parsed.hostname) class ClosestHostEvent(DictHostEvent): # if a host/path/url isn't specified, this event type grabs it from the closest parent - # inherited by FINDING and VULNERABILITY + # inherited by FINDING def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.host: @@ -1094,9 +1110,10 @@ def __init__(self, *args, **kwargs): parent_path = parent.data.get("path", None) if parent_path is not None: self.data["path"] = parent_path - # inherit closest host + # inherit closest host+port if parent.host: self.data["host"] = str(parent.host) + self._port = parent.port # we do this to refresh the hash self.data = self.data break @@ -1105,8 +1122,13 @@ def __init__(self, *args, **kwargs): raise ValueError(f"No host was found in event parents: {self.get_parents()}. Host must be specified!") -class DictPathEvent(DictEvent): +class DictPathEvent(DictHostEvent): + __slots__ = [ + "_data_path", + ] + def sanitize_data(self, data): + data = super().sanitize_data(data) new_data = dict(data) new_data["path"] = str(new_data["path"]) file_blobs = getattr(self.scan, "_file_blobs", False) @@ -1145,6 +1167,24 @@ class ASN(DictEvent): _always_emit = True _quick_emit = True + def sanitize_data(self, data): + # accept bare int (from make_event(12345, "ASN")) or dict (from JSON round-trip) + if isinstance(data, int): + data = {"asn": data} + if not isinstance(data, dict) or "asn" not in data: + raise ValidationError(f"Invalid ASN data (expected dict with 'asn' key): {data}") + data["asn"] = int(data["asn"]) + return data + + def _data_id(self): + return str(self.data["asn"]) + + def _pretty_string(self): + return str(self.data["asn"]) + + def _data_human(self): + return f"AS{self.data['asn']}" + class CODE_REPOSITORY(DictHostEvent): _always_emit = True @@ -1156,6 +1196,9 @@ class _data_validator(BaseModel): def _pretty_string(self): return self.data["url"] + def _data_human(self): + return self.data["url"] + class IP_ADDRESS(BaseEvent): def __init__(self, *args, **kwargs): @@ -1234,6 +1277,9 @@ def _words(self): class OPEN_TCP_PORT(BaseEvent): + # we generally don't care about open ports on affiliates + _always_emit_tags = ["seed"] + def sanitize_data(self, data): return validators.validate_open_port(data) @@ -1251,19 +1297,32 @@ class OPEN_UDP_PORT(OPEN_TCP_PORT): pass -class URL_UNVERIFIED(BaseEvent): +class URL_UNVERIFIED(DictHostEvent): _status_code_regex = re.compile(r"^status-(\d{1,3})$") + __slots__ = [ + "web_spider_distance", + "url_extension", + "num_redirects", + ] + def __init__(self, *args, **kwargs): + self.web_spider_distance = 0 super().__init__(*args, **kwargs) self.num_redirects = getattr(self.parent, "num_redirects", 0) + def _data_load(self, data): + # accept a bare URL string and wrap it into a dict + if isinstance(data, str): + return {"url": data} + return data + def _data_id(self): - data = super()._data_id() + url = self.url # remove the querystring for URL/URL_UNVERIFIED events, because we will conditionally add it back in (based on settings) if self.__class__.__name__.startswith("URL") and self.scan is not None: - prefix = data.split("?")[0] + prefix = url.split("?")[0] # consider spider-danger tag when deduping if "spider-danger" in self.tags: @@ -1279,11 +1338,13 @@ def _data_id(self): cleaned_query = "&".join( f"{key}={','.join(sorted(values))}" for key, values in sorted(query_dict.items()) ) - data = f"{prefix}:{self.parsed_url.scheme}:{self.parsed_url.netloc}:{self.parsed_url.path}:{cleaned_query}" - return data + url = f"{prefix}:{self.parsed_url.scheme}:{self.parsed_url.netloc}:{self.parsed_url.path}:{cleaned_query}" + return url def sanitize_data(self, data): - self.parsed_url = self.validators.validate_url_parsed(data) + url = data.get("url", "") + self.parsed_url = self.validators.validate_url_parsed(url) + data["url"] = self.parsed_url.geturl() # special handling of URL extensions if self.parsed_url is not None: @@ -1301,9 +1362,28 @@ def sanitize_data(self, data): else: self.add_tag("endpoint") - data = self.parsed_url.geturl() return data + @property + def pretty_string(self): + return self.url + + def _data_human(self): + parts = [] + status = self.http_status + if status: + parts.append(f"[{status}]") + parts.append(self.url) + if status and str(status).startswith("3"): + location = self.data.get("redirect_location", "") + if location: + parts.append(f"-> {location}") + else: + title = self.http_title + if title: + parts.append(f"- [{title}]") + return " ".join(parts) + def add_tag(self, tag): self_url = getattr(self, "parsed_url", "") self_host = getattr(self, "host", "") @@ -1351,12 +1431,31 @@ def _host(self): @property def http_status(self): + status_code = self.data.get("status_code", 0) + if status_code: + return int(status_code) for t in self.tags: match = self._status_code_regex.match(t) if match: return int(match.groups()[0]) return 0 + @property + def http_title(self): + return self.data.get("http_title", "") + + @http_title.setter + def http_title(self, value): + self.data["http_title"] = value + + @property + def redirect_location(self): + return self.data.get("redirect_location", "") + + @redirect_location.setter + def redirect_location(self, value): + self.data["redirect_location"] = value + class URL(URL_UNVERIFIED): def __init__(self, *args, **kwargs): @@ -1367,17 +1466,8 @@ def __init__(self, *args, **kwargs): 'Must specify HTTP status tag for URL event, e.g. "status-200". Use URL_UNVERIFIED if the URL is unvisited.' ) - @property - def resolved_hosts(self): - # TODO: remove this when we rip out httpx - return {".".join(i.split("-")[1:]) for i in self.tags if i.startswith("ip-")} - @property - def pretty_string(self): - return self.data - - -class STORAGE_BUCKET(DictEvent, URL_UNVERIFIED): +class STORAGE_BUCKET(URL_UNVERIFIED): _always_emit = True _suppress_chain_dupes = True @@ -1391,6 +1481,9 @@ def sanitize_data(self, data): data["name"] = data["name"].lower() return data + def _data_human(self): + return f"{self.data['name']} ({self.data['url']})" + def _words(self): return self.data["name"] @@ -1400,6 +1493,10 @@ class URL_HINT(URL_UNVERIFIED): class WEB_PARAMETER(DictHostEvent): + __slots__ = [ + "envelopes", + ] + @property def children(self): # if we have any subparams, raise a new WEB_PARAMETER for each one @@ -1421,6 +1518,7 @@ def children(self): return children def sanitize_data(self, data): + data = super().sanitize_data(data) original_value = data.get("original_value", None) if original_value is not None: try: @@ -1454,6 +1552,29 @@ def _outgoing_dedup_hash(self, event): def _url(self): return self.data["url"] + def _data_human(self): + param_type = self.data.get("type", "") + name = self.data.get("name", "") + original_value = self.data.get("original_value", "") + url = self.data.get("url", "") + description = self.data.get("description", "") + additional_params = self.data.get("additional_params", {}) + parts = [] + if param_type: + parts.append(f"[{param_type}]") + if original_value: + parts.append(f"{name}={original_value}") + else: + parts.append(name) + if description: + parts.append(f"- {description}") + if additional_params: + param_names = ", ".join(sorted(additional_params.keys())) + parts.append(f"Additional Params: [{param_names}]") + if url: + parts.append(f"({url})") + return " ".join(parts) + def __str__(self): max_event_len = 200 d = str(self.data) @@ -1473,7 +1594,7 @@ def _words(self): return extract_words(self.host_stem) -class HTTP_RESPONSE(URL_UNVERIFIED, DictEvent): +class HTTP_RESPONSE(URL_UNVERIFIED): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # count number of consecutive redirects @@ -1518,6 +1639,34 @@ def _words(self): def _pretty_string(self): return f"{self.data['hash']['header_mmh3']}:{self.data['hash']['body_mmh3']}" + def _data_human(self): + parts = [] + status = self.http_status + if status: + parts.append(f"[{status}]") + method = self.data.get("method", "") + if method: + parts.append(method) + parts.append(self.data.get("url", "")) + if status and str(status).startswith("3"): + location = self.redirect_location + if location: + parts.append(f"-> {location}") + else: + title = self.http_title + if title: + parts.append(f"- [{title}]") + content_length = self.data.get("content_length", 0) + if content_length: + parts.append(f"({bytes_to_human(content_length)})") + return " ".join(parts) + + def _minimize(self): + super()._minimize() + if self._module_consumers <= 0: + self._data.pop("body", None) + self._data.pop("raw_header", None) + @property def raw_response(self): """ @@ -1556,49 +1705,99 @@ def redirect_location(self): return location -class VULNERABILITY(ClosestHostEvent): +class FINDING(ClosestHostEvent): _always_emit = True _quick_emit = True + + class _stats_class: + _severity_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] + + def __init__(self): + self.count = 0 + self.severities = {} + + def increment(self, event): + self.count += 1 + sev = event.data.get("severity", "UNKNOWN") + try: + self.severities[sev] += 1 + except KeyError: + self.severities[sev] = 1 + + def format(self, event_type): + if not self.severities: + return f"{event_type}: {self.count}" + parts = [] + for sev in self._severity_order: + n = self.severities.get(sev, 0) + if n: + parts.append(f"{n} {sev}") + for sev, n in sorted(self.severities.items()): + if sev not in self._severity_order and n: + parts.append(f"{n} {sev}") + return f"{event_type}: {self.count} ({', '.join(parts)})" + severity_colors = { "CRITICAL": "🟪", "HIGH": "🟥", "MEDIUM": "🟧", "LOW": "🟨", - "UNKNOWN": "⬜", + "INFO": "⬜", + } + + confidence_colors = { + "CONFIRMED": "🟣", + "HIGH": "🔴", + "MEDIUM": "🟠", + "LOW": "🟡", + "UNKNOWN": "⚪", } def sanitize_data(self, data): - self.add_tag(data["severity"].lower()) + data = super().sanitize_data(data) + self.add_tag(f"severity-{data['severity'].lower()}") + self.add_tag(f"confidence-{data['confidence'].lower()}") return data class _data_validator(BaseModel): host: Optional[str] = None severity: str + name: str description: str + confidence: str url: Optional[str] = None + full_url: Optional[str] = None path: Optional[str] = None + cves: Optional[list[str]] = None _validate_url = field_validator("url")(validators.validate_url) _validate_host = field_validator("host")(validators.validate_host) _validate_severity = field_validator("severity")(validators.validate_severity) + _validate_confidence = field_validator("confidence")(validators.validate_confidence) def _pretty_string(self): - return f"[{self.data['severity']}] {self.data['description']}" + severity = self.data["severity"] + confidence = self.data["confidence"] + description = self.data["description"] + # Add bold formatting for CONFIRMED confidence + if confidence == "CONFIRMED": + confidence_str = f"[\033[1m{confidence}\033[0m]" + else: + confidence_str = f"[{confidence}]" + return f"Severity: [{severity}] Confidence: {confidence_str} {description}" -class FINDING(ClosestHostEvent): - _always_emit = True - _quick_emit = True - - class _data_validator(BaseModel): - host: Optional[str] = None - description: str - url: Optional[str] = None - path: Optional[str] = None - _validate_url = field_validator("url")(validators.validate_url) - _validate_host = field_validator("host")(validators.validate_host) - - def _pretty_string(self): - return self.data["description"] + def _data_human(self): + parts = [] + parts.append(f"Severity: [{self.data['severity']}]") + parts.append(f"Confidence: [{self.data['confidence']}]") + parts.append(self.data["description"]) + url = self.data.get("url", "") + if url and url not in self.data["description"]: + parts.append(f"({url})") + cves = self.data.get("cves", []) + if cves: + parts.append(f"[{', '.join(cves)}]") + return " ".join(parts) class TECHNOLOGY(DictHostEvent): @@ -1609,6 +1808,11 @@ class _data_validator(BaseModel): _validate_url = field_validator("url")(validators.validate_url) _validate_host = field_validator("host")(validators.validate_host) + def _sanitize_data(self, data): + data = super()._sanitize_data(data) + data["technology"] = data["technology"].lower() + return data + def _data_id(self): # dedupe by host+port+tech tech = self.data.get("technology", "") @@ -1617,17 +1821,12 @@ def _data_id(self): def _pretty_string(self): return self.data["technology"] - -class VHOST(DictHostEvent): - class _data_validator(BaseModel): - host: str - vhost: str - url: Optional[str] = None - _validate_url = field_validator("url")(validators.validate_url) - _validate_host = field_validator("host")(validators.validate_host) - - def _pretty_string(self): - return self.data["vhost"] + def _data_human(self): + tech = self.data["technology"] + url = self.data.get("url", "") + if url: + return f"{tech} ({url})" + return tech class PROTOCOL(DictHostEvent): @@ -1651,11 +1850,35 @@ def port(self): def _pretty_string(self): return self.data["protocol"] + def _data_human(self): + protocol = self.data["protocol"] + port = self.data.get("port") + banner = self.data.get("banner", "") + if port: + result = f"{protocol}/{port}" + else: + result = protocol + if banner: + result += f" - {banner}" + return result + class GEOLOCATION(BaseEvent): _always_emit = True _quick_emit = True + def _data_human(self): + country = self.data.get("country_name", "") + region = self.data.get("region_name", "") + city = self.data.get("city_name", "") or self.data.get("city", "") + lat = self.data.get("latitude", "") + lon = self.data.get("longitude", "") + location_parts = [p for p in (city, region, country) if p] + result = ", ".join(location_parts) + if lat and lon: + result += f" ({lat}, {lon})" + return result if result else super()._data_human() + class PASSWORD(BaseEvent): _always_emit = True @@ -1677,16 +1900,50 @@ class SOCIAL(DictHostEvent): _quick_emit = True _scope_distance_increment_same_host = True + def _data_human(self): + platform = self.data.get("platform", "") + profile_name = self.data.get("profile_name", "") + url = self.data.get("url", "") + parts = [] + if platform: + parts.append(f"{platform}:") + parts.append(profile_name) + if url: + parts.append(f"({url})") + return " ".join(parts) + -class WEBSCREENSHOT(DictPathEvent, DictHostEvent): +class WEBSCREENSHOT(DictPathEvent): _always_emit = True _quick_emit = True + def _data_human(self): + return f"{self.data.get('url', '')} Saved to: {self.data.get('path', '')}" + class AZURE_TENANT(DictEvent): _always_emit = True _quick_emit = True + def _data_human(self): + max_domains = 20 + tenant_names = self.data.get("tenant-names", []) + tenant_id = self.data.get("tenant-id", "") + domains = self.data.get("domains", []) + parts = [] + if tenant_names: + parts.append(", ".join(tenant_names)) + if tenant_id: + parts.append(f"({tenant_id})") + if domains: + if len(domains) <= max_domains: + parts.append(f"- {', '.join(domains)}") + else: + shown = ", ".join(domains[:max_domains]) + hidden = len(domains) - max_domains + parts.append(f"- {shown} (hiding {hidden} additional domains)") + return " ".join(parts) if parts else super()._data_human() + class WAF(DictHostEvent): _always_emit = True @@ -1703,8 +1960,25 @@ class _data_validator(BaseModel): def _pretty_string(self): return self.data["waf"] + def _data_human(self): + waf = self.data["waf"] + info = self.data.get("info", "") + url = self.data.get("url", "") + if info: + waf += f" - {info}" + if url: + waf += f" ({url})" + return waf + class FILESYSTEM(DictPathEvent): + def _data_human(self): + path = self.data.get("path", "") + description = self.data.get("magic_description", "") + if description: + return f"{path} ({description})" + return path + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self._data_path.is_file(): @@ -1731,13 +2005,20 @@ def __init__(self, *args, **kwargs): class RAW_DNS_RECORD(DictHostEvent, DnsEvent): # don't emit raw DNS records for affiliates - _always_emit_tags = ["target"] + _always_emit_tags = ["seed"] + + def _data_human(self): + rdtype = self.data.get("type", "") + host = self.data.get("host", "") + answer = self.data.get("answer", "") + return f"{rdtype} {host} -> {answer}" class MOBILE_APP(DictEvent): _always_emit = True def _sanitize_data(self, data): + data = super()._sanitize_data(data) if isinstance(data, str): data = {"url": data} if "url" not in data: @@ -1761,6 +2042,13 @@ def _sanitize_data(self, data): def _pretty_string(self): return self.data["url"] + def _data_human(self): + app_id = self.data.get("id", "") + url = self.data.get("url", "") + if app_id and url: + return f"{app_id} ({url})" + return url or app_id + def update_event( event, @@ -1819,7 +2107,6 @@ def make_event( module=None, scan=None, tags=None, - confidence=100, dummy=False, internal=None, ): @@ -1838,7 +2125,6 @@ def make_event( scan (Scan, optional): BBOT Scan object associated with the event. scans (List[Scan], optional): Multiple BBOT Scan objects, primarily used for unserialization. tags (Union[str, List[str]], optional): Descriptive tags for the event, as a list or a single string. - confidence (int, optional): Confidence level for the event, on a scale of 1-100. Defaults to 100. dummy (bool, optional): Disables data validations if set to True. Defaults to False. internal (Any, optional): Makes the event internal if set to True. Defaults to None. @@ -1872,7 +2158,7 @@ def make_event( if not dummy: log.debug(f'Autodetected event type "{event_type}" based on data: "{data}"') - event_type = str(event_type).strip().upper() + event_type = sys.intern(str(event_type).strip().upper()) # Catch these common whoopsies if event_type in ("DNS_NAME", "IP_ADDRESS"): @@ -1912,13 +2198,12 @@ def make_event( module=module, scan=scan, tags=tags, - confidence=confidence, _dummy=dummy, _internal=internal, ) -def event_from_json(j, siem_friendly=False): +def event_from_json(j): """ Creates an event object from a JSON dictionary. @@ -1945,14 +2230,15 @@ def event_from_json(j, siem_friendly=False): kwargs = { "event_type": event_type, "tags": j.get("tags", []), - "confidence": j.get("confidence", 100), "context": j.get("discovery_context", None), "dummy": True, } - if siem_friendly: - data = j["data"][event_type] - else: - data = j["data"] + data = j.get("data_json", None) + if data is None: + data = j.get("data", None) + if data is None: + json_pretty = json.dumps(j, indent=2) + raise ValueError(f"data or data_json must be provided. JSON: {json_pretty}") kwargs["data"] = data event = make_event(**kwargs) event_uuid = j.get("uuid", None) @@ -1961,7 +2247,19 @@ def event_from_json(j, siem_friendly=False): resolved_hosts = j.get("resolved_hosts", []) event._resolved_hosts = set(resolved_hosts) - event.timestamp = datetime.datetime.fromisoformat(j["timestamp"]) + + http_title = j.get("http_title", "") + if http_title: + try: + event.http_title = http_title + except AttributeError: + pass + + # accept both isoformat and unix timestamp + try: + event.timestamp = datetime.datetime.fromtimestamp(j["timestamp"], ZoneInfo("UTC")) + except Exception: + event.timestamp = datetime.datetime.fromisoformat(j["timestamp"]) event.scope_distance = j["scope_distance"] parent_id = j.get("parent", None) if parent_id is not None: diff --git a/bbot/core/event/helpers.py b/bbot/core/event/helpers.py index 524eccbcd8..48c440f691 100644 --- a/bbot/core/event/helpers.py +++ b/bbot/core/event/helpers.py @@ -9,6 +9,20 @@ bbot_event_seeds = {} +# Pre-compute sorted event classes for performance +# This is computed once when the module is loaded instead of on every EventSeed() call +def _get_sorted_event_classes(): + """ + Sort event classes by priority (higher priority first). + This ensures specific patterns like ASN:12345 are checked before broad patterns like hostname:port. + """ + return sorted(bbot_event_seeds.items(), key=lambda x: getattr(x[1], "priority", 5), reverse=True) + + +# This will be populated after all event seed classes are registered +_sorted_event_classes = None + + """ An "Event Seed" is a lightweight event containing only the minimum logic required to: - parse input to determine the event type + data @@ -18,6 +32,19 @@ It's useful for quickly parsing target lists without the cpu+memory overhead of creating full-fledged BBOT events Not every type of BBOT event needs to be represented here. Only ones that are meant to be targets. + +PRIORITY SYSTEM: +Event seeds support a priority system to control the order in which regex patterns are checked. +This prevents conflicts where one event type's regex might incorrectly match another type's input. + +Priority values: +- Higher numbers = checked first +- Default priority = 5 +- Range: 1-10 + +To set priority on an event seed class: + class MyEventSeed(BaseEventSeed): + priority = 8 # Higher than default, will be checked before most others """ @@ -27,17 +54,25 @@ class EventSeedRegistry(type): """ def __new__(mcs, name, bases, attrs): - global bbot_event_seeds + global bbot_event_seeds, _sorted_event_classes cls = super().__new__(mcs, name, bases, attrs) # Don't register the base EventSeed class if name != "BaseEventSeed": bbot_event_seeds[cls.__name__] = cls + # Recompute sorted classes whenever a new event seed is registered + _sorted_event_classes = _get_sorted_event_classes() return cls def EventSeed(input): input = smart_encode_punycode(smart_decode(input).strip()) - for _, event_class in bbot_event_seeds.items(): + + # Use pre-computed sorted event classes for better performance + global _sorted_event_classes + if _sorted_event_classes is None: + _sorted_event_classes = _get_sorted_event_classes() + + for _, event_class in _sorted_event_classes: if hasattr(event_class, "precheck"): if event_class.precheck(input): return event_class(input) @@ -53,6 +88,7 @@ def EventSeed(input): class BaseEventSeed(metaclass=EventSeedRegistry): regexes = [] _target_type = "TARGET" + priority = 5 # Default priority for event seed matching (1-10, higher = checked first) __slots__ = ["data", "host", "port", "input"] @@ -76,6 +112,9 @@ def _sanitize_and_extract_host(self, data): """ return data, None, None + async def _generate_children(self, ssl_verify=False): + return [] + def _override_input(self, input): return self.data @@ -83,6 +122,12 @@ def _override_input(self, input): def type(self): return self.__class__.__name__ + @property + def url(self): + if self.type == "URL_UNVERIFIED": + return self.data + return "" + @cached_property def _hash(self): return hash(self.input) @@ -143,6 +188,7 @@ def _sanitize_and_extract_host(data): class OPEN_TCP_PORT(BaseEventSeed): regexes = regexes.event_type_regexes["OPEN_TCP_PORT"] + priority = 1 # Low priority: broad hostname:port pattern should be checked after specific patterns @staticmethod def _sanitize_and_extract_host(data): @@ -236,3 +282,33 @@ def _override_input(self, input): @staticmethod def handle_match(match): return match.group(1) + + +class ASN(BaseEventSeed): + regexes = (re.compile(r"^(?:ASN|AS):?(\d+)$", re.I),) # adjust regex to match ASN:17178 AS17178 + priority = 10 # High priority + + def _override_input(self, input): + return f"ASN:{self.data}" + + # ASNs are essentially just a superset of IP_RANGES. + # This method resolves the ASN to a list of IP_RANGES using the ASN API, and then adds the cidr string as a child event seed. + # These will later be automatically resolved to an IP_RANGE event seed and added to the target. + async def _generate_children(self, ssl_verify=False): + from asndb import ASNDB + + client = ASNDB(verify=ssl_verify) + asn_data = await client.lookup_asn(str(self.data), include_subnets=True) + children = [] + if asn_data: + subnets = asn_data.get("subnets") + if isinstance(subnets, str): + subnets = [subnets] + if subnets: + for cidr in subnets: + children.append(cidr) + return children + + @staticmethod + def handle_match(match): + return match.group(1) diff --git a/bbot/core/flags.py b/bbot/core/flags.py index 3391b48635..26ee48f840 100644 --- a/bbot/core/flags.py +++ b/bbot/core/flags.py @@ -1,25 +1,25 @@ flag_descriptions = { "active": "Makes active connections to target systems", "affiliates": "Discovers affiliated hostnames/domains", - "aggressive": "Generates a large amount of network traffic", "baddns": "Runs all modules from the DNS auditing tool BadDNS", "cloud-enum": "Enumerates cloud resources", "code-enum": "Find public code repositories and search them for secrets etc.", - "deadly": "Highly aggressive", "download": "Modules that download files, apps, or repositories", "email-enum": "Enumerates email addresses", "iis-shortnames": "Scans for IIS Shortname vulnerability", + "invasive": "Intrusive or potentially destructive", + "loud": "Generates a large amount of network traffic", "passive": "Never connects to target systems", + "safe": "Non-intrusive and non-destructive", "portscan": "Discovers open ports", "report": "Generates a report at the end of the scan", - "safe": "Non-intrusive, safe to run", "service-enum": "Identifies protocols running on open ports", "slow": "May take a long time to complete", "social-enum": "Enumerates social media", "subdomain-enum": "Enumerates subdomains", "subdomain-hijack": "Detects hijackable subdomains", - "web-basic": "Basic, non-intrusive web scan functionality", + "web": "Non-intrusive web scan functionality", + "web-heavy": "More advanced web scanning functionality", "web-paramminer": "Discovers HTTP parameters through brute-force", "web-screenshots": "Takes screenshots of web pages", - "web-thorough": "More advanced web scanning functionality", } diff --git a/bbot/core/helpers/asn.py b/bbot/core/helpers/asn.py new file mode 100644 index 0000000000..d7693bfcce --- /dev/null +++ b/bbot/core/helpers/asn.py @@ -0,0 +1,83 @@ +import logging + +log = logging.getLogger("bbot.core.helpers.asn") + + +class ASNHelper: + """Thin wrapper around the asndb library for ASN lookups. + + Delegates all HTTP, caching, and retry logic to the asndb library. + Normalizes response dicts to the BBOT-internal format with keys: + asn, subnets, name, description, country + """ + + UNKNOWN_ASN = { + "asn": 0, + "subnets": [], + "name": "Unknown", + "description": "Unknown ASN", + "country": "Unknown", + } + + def __init__(self, parent_helper): + self.parent_helper = parent_helper + self._client = None + + @property + def client(self): + if self._client is None: + from asndb import ASNDB + + ssl_verify = self.parent_helper.web_config.get("ssl_verify", False) + self._client = ASNDB(verify=ssl_verify) + return self._client + + def _normalize(self, response): + """Convert asndb response dict to BBOT internal format.""" + if response is None or response.get("asn", 0) == 0: + return self.UNKNOWN_ASN + return { + "asn": int(response.get("asn", 0)), + "subnets": response.get("subnets", []), + "name": response.get("asn_name") or response.get("name") or "", + "description": response.get("org") or response.get("description") or "", + "country": response.get("country") or "", + } + + async def ip_to_subnets(self, ip): + """Return ASN info for an IP address.""" + try: + response = await self.client.lookup_ip(str(ip), include_subnets=True) + except Exception as e: + log.warning(f"ASN lookup failed for IP {ip}: {e}") + return self.UNKNOWN_ASN + return self._normalize(response) + + async def asn_to_subnets(self, asn): + """Return ASN info (including subnets) for an ASN number.""" + if isinstance(asn, str): + try: + asn = int(asn.lower().lstrip("as")) + except ValueError: + log.warning(f"Invalid ASN format: {asn}") + return self.UNKNOWN_ASN + try: + response = await self.client.lookup_asn(str(asn), include_subnets=True) + except Exception as e: + log.warning(f"ASN lookup failed for AS{asn}: {e}") + return self.UNKNOWN_ASN + return self._normalize(response) + + async def cleanup(self): + """Clean up the asndb client.""" + if self._client is not None: + try: + await self._client.cleanup() + except Exception: + pass + self._client = None + # Reset the asndb global singleton so the next ASNDB() call + # creates a fresh client instead of returning the closed one + import asndb.asndb + + asndb.asndb.asndb_client = None diff --git a/bbot/core/helpers/async_helpers.py b/bbot/core/helpers/async_helpers.py index a9d8adc6fe..8c95dc7981 100644 --- a/bbot/core/helpers/async_helpers.py +++ b/bbot/core/helpers/async_helpers.py @@ -133,6 +133,25 @@ def async_to_sync_gen(async_gen): yield loop.run_until_complete(async_gen.__anext__()) except StopAsyncIteration: pass + finally: + # Explicitly close the async generator so its finally block + # (e.g. Scanner.async_start's cleanup) runs while the loop + # is still alive, not deferred to interpreter shutdown. + with suppress(BaseException): + loop.run_until_complete(async_gen.aclose()) + # Cancel any remaining pending tasks to prevent + # "Task was destroyed but it is pending!" warnings + pending = [t for t in asyncio.all_tasks(loop) if not t.done()] + for t in pending: + t.cancel() + if pending: + with suppress(BaseException): + loop.run_until_complete( + asyncio.wait_for( + asyncio.gather(*pending, return_exceptions=True), + timeout=5, + ) + ) def async_cachedmethod(cache, key=keys.hashkey): diff --git a/bbot/core/helpers/depsinstaller/installer.py b/bbot/core/helpers/depsinstaller/installer.py index 1348ed077c..d3ac8a3297 100644 --- a/bbot/core/helpers/depsinstaller/installer.py +++ b/bbot/core/helpers/depsinstaller/installer.py @@ -426,8 +426,10 @@ def ensure_root(self, message=""): with self.ensure_root_lock: # first check if the environment variable is set _sudo_password = os.environ.get("BBOT_SUDO_PASS", None) - if _sudo_password is not None or os.geteuid() == 0 or can_sudo_without_password(): - # if we're already root or we can sudo without a password, there's no need to prompt + if _sudo_password is not None: + self._sudo_password = _sudo_password + return + if os.geteuid() == 0 or can_sudo_without_password(): return if message: @@ -435,13 +437,34 @@ def ensure_root(self, message=""): while not self._sudo_password: # sleep for a split second to flush previous log messages sleep(0.1) - _sudo_password = getpass.getpass(prompt="[USER] Please enter sudo password: ") + try: + _sudo_password = getpass.getpass(prompt="[USER] Please enter sudo password: ") + except OSError: + log.warning("Unable to read sudo password (no TTY). Set BBOT_SUDO_PASS env var.") + return if self.parent_helper.verify_sudo_password(_sudo_password): log.success("Authentication successful") self._sudo_password = _sudo_password else: log.warning("Incorrect password") + def _core_dep_satisfied(self, command): + """Check if a core dependency is satisfied. + + For normal binary deps, check if the command exists on PATH. + For special entries like openssl_dev_headers, use a custom check. + """ + if command == "openssl_dev_headers": + # check for openssl headers by looking for the pkg-config file or header + return any( + Path(p).exists() + for p in [ + "/usr/include/openssl/ssl.h", + "/usr/local/include/openssl/ssl.h", + ] + ) or bool(self.parent_helper.which("openssl")) + return bool(self.parent_helper.which(command)) + async def install_core_deps(self): # skip if we've already successfully installed core deps for this definition core_deps_hash = str(mmh3.hash(orjson.dumps(self.CORE_DEPS, option=orjson.OPT_SORT_KEYS))) @@ -453,18 +476,25 @@ async def install_core_deps(self): to_install = set() to_install_friendly = set() playbook = [] - self._install_sudo_askpass() - # ensure tldextract data is cached - self.parent_helper.tldextract("evilcorp.co.uk") - # install any missing commands + # check which commands are missing for command, package_name_or_playbook in self.CORE_DEPS.items(): - if not self.parent_helper.which(command): - to_install_friendly.add(command) - if isinstance(package_name_or_playbook, str): - to_install.add(package_name_or_playbook) - else: - playbook.extend(package_name_or_playbook) - # install ansible community.general collection + if self._core_dep_satisfied(command): + continue + to_install_friendly.add(command) + if isinstance(package_name_or_playbook, str): + to_install.add(package_name_or_playbook) + else: + playbook.extend(package_name_or_playbook) + # construct ansible playbook + if to_install: + playbook.append( + { + "name": "Install Core BBOT Dependencies", + "package": {"name": list(to_install), "state": "present"}, + "become": True, + } + ) + # install ansible community.general collection if needed overall_success = True if not self.setup_status.get("ansible:community.general", False): log.info("Installing Ansible Community General Collection") @@ -478,17 +508,12 @@ async def install_core_deps(self): f"Failed to install Ansible Community.General Collection (return code {err.returncode}): {err.stderr}" ) overall_success = False - # construct ansible playbook - if to_install: - playbook.append( - { - "name": "Install Core BBOT Dependencies", - "package": {"name": list(to_install), "state": "present"}, - "become": True, - } - ) - # run playbook + # only run ansible if there's actually something to install if playbook: + self._install_sudo_askpass() + # ensure tldextract data is cached + self.parent_helper.tldextract("evilcorp.co.uk") + # run playbook log.info(f"Installing core BBOT dependencies: {','.join(sorted(to_install_friendly))}") self.ensure_root() success, _ = self.ansible_run(tasks=playbook) diff --git a/bbot/core/helpers/dns/brute.py b/bbot/core/helpers/dns/brute.py index 71424c5a87..d243073a18 100644 --- a/bbot/core/helpers/dns/brute.py +++ b/bbot/core/helpers/dns/brute.py @@ -49,7 +49,7 @@ async def dnsbrute(self, module, domain, subdomains, type=None): wildcard_domains = await self.parent_helper.dns.is_wildcard_domain(domain, (type, "CNAME")) wildcard_rdtypes = set() - for domain, rdtypes in wildcard_domains.items(): + for wildcard_domain, rdtypes in wildcard_domains.items(): wildcard_rdtypes.update(rdtypes) if wildcard_domains: self.log.hugewarning( @@ -57,8 +57,8 @@ async def dnsbrute(self, module, domain, subdomains, type=None): ) return [] - canaries = self.gen_random_subdomains(self.num_canaries) - canaries_list = list(canaries) + canaries_list = list(self.gen_random_subdomains(self.num_canaries)) + canary_set = set(canaries_list) canaries_pre = canaries_list[: int(self.num_canaries / 2)] canaries_post = canaries_list[int(self.num_canaries / 2) :] # sandwich subdomains between canaries @@ -67,8 +67,8 @@ async def dnsbrute(self, module, domain, subdomains, type=None): results = [] canaries_triggered = [] async for hostname, ip, rdtype in self._massdns(module, domain, subdomains, rdtype=type): - sub = hostname.split(domain)[0] - if sub in canaries: + sub = hostname.split(domain)[0].rstrip(".") + if sub in canary_set: canaries_triggered.append(sub) else: results.append(hostname) diff --git a/bbot/core/helpers/dns/engine.py b/bbot/core/helpers/dns/engine.py index 1c98f9a9e3..d2e56dc207 100644 --- a/bbot/core/helpers/dns/engine.py +++ b/bbot/core/helpers/dns/engine.py @@ -72,7 +72,7 @@ def __init__(self, socket_path, config={}, debug=False): self.wildcard_ignore = [] self.wildcard_ignore = tuple([str(d).strip().lower() for d in self.wildcard_ignore]) self.wildcard_tests = self.dns_config.get("wildcard_tests", 5) - self._wildcard_cache = {} + self._wildcard_cache = LRUCache(maxsize=10000) # since wildcard detection takes some time, This is to prevent multiple # modules from kicking off wildcard detection for the same domain at the same time self._wildcard_lock = NamedLock() @@ -81,10 +81,10 @@ def __init__(self, socket_path, config={}, debug=False): self._last_dns_success = None self._last_connectivity_warning = time.time() # keeps track of warnings issued for wildcard detection to prevent duplicate warnings - self._dns_warnings = set() - self._errors = {} + self._dns_warnings = LRUCache(maxsize=10000) + self._errors = LRUCache(maxsize=10000) self._debug = self.dns_config.get("debug", False) - self._dns_cache = LRUCache(maxsize=10000) + self._dns_cache = LRUCache(maxsize=100000) async def resolve(self, query, **kwargs): """Resolve DNS names and IP addresses to their corresponding results. @@ -221,7 +221,7 @@ async def _resolve_hostname(self, query, **kwargs): self.log.verbose( f'Aborting future {rdtype} queries to "{parent}" because error count ({error_count:,}) exceeded abort threshold ({self.abort_threshold:,})' ) - self._dns_warnings.add(parent_hash) + self._dns_warnings[parent_hash] = True return results, errors results = await self._catch(self.resolver.resolve, query, **kwargs) if use_cache: diff --git a/bbot/core/helpers/helper.py b/bbot/core/helpers/helper.py index 1f5762214e..d86b3ada02 100644 --- a/bbot/core/helpers/helper.py +++ b/bbot/core/helpers/helper.py @@ -6,6 +6,7 @@ from concurrent.futures import ProcessPoolExecutor from . import misc +from .asn import ASNHelper from .dns import DNSHelper from .web import WebHelper from .diff import HttpCompare @@ -13,6 +14,7 @@ from .wordcloud import WordCloud from .interactsh import Interactsh from .yara_helper import YaraHelper +from .simhash import SimHashHelper from .depsinstaller import DepsInstaller from .async_helpers import get_event_loop @@ -87,9 +89,11 @@ def __init__(self, preset): self.re = RegexHelper(self) self.yara = YaraHelper(self) + self.simhash = SimHashHelper() self._dns = None self._web = None self._cloudcheck = None + self._asn = None self.config_aware_validators = self.validators.Validators(self) self.depsinstaller = DepsInstaller(self) self.word_cloud = WordCloud(self) @@ -107,6 +111,12 @@ def web(self): self._web = WebHelper(self) return self._web + @property + def asn(self): + if self._asn is None: + self._asn = ASNHelper(self) + return self._asn + @property def cloudcheck(self): if self._cloudcheck is None: diff --git a/bbot/core/helpers/interactsh.py b/bbot/core/helpers/interactsh.py index 5c2eb0e526..a7d5e59634 100644 --- a/bbot/core/helpers/interactsh.py +++ b/bbot/core/helpers/interactsh.py @@ -195,6 +195,10 @@ async def deregister(self): if self._poll_task is not None: self._poll_task.cancel() + try: + await self._poll_task + except (asyncio.CancelledError, Exception): + pass if "success" not in getattr(r, "text", ""): raise InteractshError(f"Failed to de-register with interactsh server {self.server}") @@ -274,21 +278,41 @@ async def poll_loop(self, callback): return await self._poll_loop(callback) async def _poll_loop(self, callback): + consecutive_failures = 0 + max_failures = 5 + log.debug(f"Starting interactsh poll loop (interval={self.poll_interval}s, server={self.server})") while 1: if self.parent_helper.scan.stopping: - await asyncio.sleep(1) - continue + log.debug("Stopping interactsh poll loop (scan is stopping)") + break data_list = [] try: data_list = await self.poll() + consecutive_failures = 0 except InteractshError as e: - log.warning(e) + consecutive_failures += 1 + if consecutive_failures == 1: + log.warning(e) + elif consecutive_failures >= max_failures: + log.error(f"Interactsh poll failed {max_failures} consecutive times, giving up: {e}") + break + else: + log.debug(f"Interactsh poll failure #{consecutive_failures}: {e}") log.trace(traceback.format_exc()) + backoff = min(self.poll_interval * (2 ** (consecutive_failures - 1)), 300) + log.debug(f"Interactsh backing off for {backoff}s after failure #{consecutive_failures}") + await asyncio.sleep(backoff) + continue if not data_list: + log.debug(f"Interactsh poll returned no interactions, sleeping {self.poll_interval}s") await asyncio.sleep(self.poll_interval) continue + log.debug(f"Interactsh poll returned {len(data_list)} interaction(s)") for data in data_list: if data: + protocol = data.get("protocol", "unknown") + full_id = data.get("full-id", "unknown") + log.debug(f"Interactsh interaction: protocol={protocol}, full-id={full_id}") await self.parent_helper.execute_sync_or_async(callback, data) def _decrypt(self, aes_key, data): diff --git a/bbot/core/helpers/misc.py b/bbot/core/helpers/misc.py index ad96cdb374..cd2011abcc 100644 --- a/bbot/core/helpers/misc.py +++ b/bbot/core/helpers/misc.py @@ -11,9 +11,11 @@ import regex as re import subprocess as sp + from pathlib import Path from contextlib import suppress from unidecode import unidecode # noqa F401 +from typing import Iterable, Awaitable, Optional from asyncio import create_task, gather, sleep, wait_for # noqa from urllib.parse import urlparse, quote, unquote, urlunparse, urljoin # noqa F401 @@ -835,7 +837,7 @@ def rand_string(length=10, digits=True, numeric_only=False): return "".join(random.choice(pool) for _ in range(length)) -def truncate_string(s: str, n: int) -> str: +def truncate_string(s: str, n: int = 200) -> str: if not isinstance(s, str): raise ValueError(f"Expected string, got {type(s)}") if len(s) > n: @@ -1116,6 +1118,32 @@ def str_or_file(s): yield s +_comment_re = re.compile(r"\s#") + + +def strip_comments(line): + """Strip #-style comments from a line. + + Handles full-line comments (``# ...``) and inline comments (``target # ...``). + The ``#`` must be preceded by whitespace to count as an inline comment, + so URL fragments like ``http://example.com/page#section`` are preserved. + + Examples: + >>> strip_comments("evilcorp.com # main domain") + 'evilcorp.com' + >>> strip_comments("# full line comment") + '' + >>> strip_comments("http://example.com/page#section") + 'http://example.com/page#section' + """ + if line.lstrip().startswith("#"): + return "" + m = _comment_re.search(line) + if m: + return line[: m.start()] + return line + + split_regex = re.compile(r"[\s,]") @@ -1126,6 +1154,7 @@ def chain_lists( remove_blank=True, validate=False, validate_chars='<>:"/\\|?*)', + _strip_comments=False, ): """Chains together list elements, allowing for entries separated by commas. @@ -1141,6 +1170,7 @@ def chain_lists( remove_blank (bool, optional): Whether to remove blank entries from the list. Defaults to True. validate (bool, optional): Whether to perform validation for undesirable characters. Defaults to False. validate_chars (str, optional): When performing validation, what additional set of characters to block (blocks non-printable ascii automatically). Defaults to '<>:"/\\|?*)' + _strip_comments (bool, optional): Whether to strip ``#``-style comments from entries and file lines. Defaults to False. Returns: list: The list of chained elements. @@ -1159,6 +1189,8 @@ def chain_lists( l = [l] final_list = {} for entry in l: + if _strip_comments: + entry = strip_comments(entry) for s in split_regex.split(entry): f = s.strip() if validate: @@ -1170,6 +1202,8 @@ def chain_lists( new_msg = str(msg).format(filename=f_path) log.info(new_msg) for line in str_or_file(f): + if _strip_comments: + line = strip_comments(line) final_list[line] = None else: final_list[f] = None @@ -2595,30 +2629,101 @@ def parse_port_string(port_string): return ports -async def as_completed(coros): +async def as_completed( + coroutines: Iterable[Awaitable], + max_concurrent: Optional[int] = 20, +): """ - Async generator that yields completed Tasks as they are completed. + Yield completed coroutines as they finish with optional concurrency limiting. + All coroutines are scheduled as tasks internally for execution. - Args: - coros (iterable): An iterable of coroutine objects or asyncio Tasks. + Guarantees cleanup: + - If the consumer breaks early or an internal cancellation is detected, all remaining + tasks are cancelled and awaited (with return_exceptions=True) to avoid + "Task exception was never retrieved" warnings. + """ + it = iter(coroutines) - Yields: - asyncio.Task: A Task object that has completed its execution. + running: set[asyncio.Task] = set() + limit = max_concurrent or float("inf") - Examples: - >>> async def main(): - ... async for task in as_completed([coro1(), coro2(), coro3()]): - ... result = task.result() - ... print(f'Task completed with result: {result}') + async def _cancel_and_drain_remaining(): + if not running: + return + for t in running: + t.cancel() + try: + await asyncio.gather(*running, return_exceptions=True) + finally: + running.clear() - >>> asyncio.run(main()) + # Prime the running set up to the concurrency limit (or all, if unlimited) + try: + while len(running) < limit: + coro = next(it) + running.add(asyncio.create_task(coro)) + except StopIteration: + pass + + # Dedup state for repeated error messages + _last_err = {"msg": None, "count": 0} + + try: + # Drain: yield completed tasks, backfill from the iterator as slots free up + while running: + done, running = await asyncio.wait(running, return_when=asyncio.FIRST_COMPLETED) + for task in done: + # Immediately backfill one slot per completed task, if more work remains + try: + coro = next(it) + running.add(asyncio.create_task(coro)) + except StopIteration: + pass + + # If task raised, handle cancellation gracefully and dedupe noisy repeats + if task.exception() is not None: + e = task.exception() + if in_exception_chain(e, (KeyboardInterrupt, asyncio.CancelledError)): + # Quietly stop if we're being cancelled + log.info("as_completed: cancellation detected; exiting early") + await _cancel_and_drain_remaining() + return + # Build a concise message + msg = f"as_completed yielded exception: {e}" + if msg == _last_err["msg"]: + _last_err["count"] += 1 + if _last_err["count"] <= 3: + log.warning(msg) + elif _last_err["count"] % 10 == 0: + log.warning(f"{msg} (repeated {_last_err['count']}x)") + else: + log.debug(msg) + else: + _last_err["msg"] = msg + _last_err["count"] = 1 + log.warning(msg) + yield task + finally: + # If the consumer breaks early or an error bubbles, ensure we don't leak tasks + await _cancel_and_drain_remaining() + + +def get_waf_strings(): + """ + Returns a list of common WAF (Web Application Firewall) detection strings. + + Returns: + list: List of WAF detection strings + + Examples: + >>> waf_strings = get_waf_strings() + >>> "The requested URL was rejected" in waf_strings + True """ - tasks = {coro if isinstance(coro, asyncio.Task) else asyncio.create_task(coro): coro for coro in coros} - while tasks: - done, _ = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED) - for task in done: - tasks.pop(task) - yield task + return [ + "The requested URL was rejected", + "This content has been blocked", + ] def clean_dns_record(record): diff --git a/bbot/core/helpers/names_generator.py b/bbot/core/helpers/names_generator.py index 9c0da33607..cc45b19cf6 100644 --- a/bbot/core/helpers/names_generator.py +++ b/bbot/core/helpers/names_generator.py @@ -42,6 +42,7 @@ "crumbly", "cryptic", "cuddly", + "cursed", "cute", "dark", "dastardly", @@ -158,6 +159,7 @@ "mysterious", "nascent", "naughty", + "nautical", "nefarious", "negligent", "neurotic", @@ -169,6 +171,7 @@ "overmedicated", "overwhelming", "overzealous", + "pacific", "paranoid", "pasty", "peckish", @@ -347,7 +350,9 @@ "brittany", "bruce", "bryan", + "buhner", "caitlyn", + "cal", "caleb", "cameron", "carl", @@ -432,6 +437,7 @@ "evan", "evelyn", "faramir", + "felix", "florence", "fox", "frances", @@ -458,6 +464,7 @@ "gollum", "grace", "gregory", + "griffey", "gus", "hagrid", "hank", @@ -472,6 +479,7 @@ "homer", "howard", "hunter", + "ichiro", "irene", "isaac", "isabella", @@ -515,6 +523,7 @@ "judy", "julia", "julie", + "julio", "justin", "karen", "katherine", @@ -547,6 +556,7 @@ "logan", "lois", "lori", + "lou", "louis", "louise", "lucius", @@ -578,6 +588,7 @@ "mildred", "milhouse", "monica", + "moose", "nancy", "natalie", "nathan", @@ -694,6 +705,8 @@ "wendy", "william", "willie", + "wilson", + "woo", "worf", "wormtongue", "xavier", diff --git a/bbot/core/helpers/simhash.py b/bbot/core/helpers/simhash.py new file mode 100644 index 0000000000..65fa72e813 --- /dev/null +++ b/bbot/core/helpers/simhash.py @@ -0,0 +1,115 @@ +import xxhash +import re + +_non_word_re = re.compile(r"[^\w]+") + + +class SimHashHelper: + def __init__(self, bits=64): + self.bits = bits + + @staticmethod + def compute_simhash(text, bits=64, truncate=True, normalization_filter=None): + """ + Static method for computing SimHash that can be used with multiprocessing. + + This method is designed to be used with run_in_executor_mp() for CPU-intensive + SimHash computations across multiple processes. + + Args: + text (str): The text to hash + bits (int): Number of bits for the hash. Defaults to 64. + truncate (bool): Whether to truncate large text for performance. Defaults to True. + normalization_filter (str): Text to remove for normalization. Defaults to None. + + Returns: + int: The SimHash fingerprint + """ + helper = SimHashHelper(bits=bits) + return helper.hash(text, truncate=truncate, normalization_filter=normalization_filter) + + def _truncate_content(self, content): + """ + Truncate large content for similarity comparison to improve performance. + + Truncation rules: + - If content <= 3072 bytes: return as-is + - If content > 3072 bytes: return first 2048 bytes + last 1024 bytes + """ + content_length = len(content) + + # No truncation needed for smaller content + if content_length <= 3072: + return content + + # Truncate: first 2048 + last 1024 bytes + first_part = content[:2048] + last_part = content[-1024:] + + return first_part + last_part + + def _normalize_text(self, text, normalization_filter): + """ + Normalize text by removing the normalization filter from the text. + """ + return text.replace(normalization_filter, "") + + def _get_features(self, text): + """Extract 3-character shingles as features""" + width = 3 + text = text.lower() + # Remove non-word characters + text = _non_word_re.sub("", text) + # Create 3-character shingles + return [text[i : i + width] for i in range(max(len(text) - width + 1, 1))] + + def _hash_feature(self, feature): + """Return a hash of a feature using xxHash""" + return xxhash.xxh64(feature.encode("utf-8")).intdigest() + + def hash(self, text, truncate=True, normalization_filter=None): + """ + Generate a SimHash fingerprint for the given text. + + Args: + text (str): The text to hash + truncate (bool): Whether to truncate large text for performance. Defaults to True. + When enabled, text larger than 4KB is truncated to first 2KB + last 1KB for comparison. + + Returns: + int: The SimHash fingerprint + """ + # Apply truncation if enabled + if truncate: + text = self._truncate_content(text) + + if normalization_filter: + text = self._normalize_text(text, normalization_filter) + + vector = [0] * self.bits + features = self._get_features(text) + + for feature in features: + hv = self._hash_feature(feature) + for i in range(self.bits): + bit = (hv >> i) & 1 + vector[i] += 1 if bit else -1 + + # Final fingerprint + fingerprint = 0 + for i, val in enumerate(vector): + if val >= 0: + fingerprint |= 1 << i + return fingerprint + + def similarity(self, hash1, hash2): + """ + Compute similarity between two SimHashes as a value between 0.0 and 1.0. + """ + # Hamming distance: count of differing bits + diff = (hash1 ^ hash2).bit_count() + return 1.0 - (diff / self.bits) + + +# Module-level alias for the static method to enable clean imports +compute_simhash = SimHashHelper.compute_simhash diff --git a/bbot/core/helpers/validators.py b/bbot/core/helpers/validators.py index 97a39fae3c..27c8bd0fea 100644 --- a/bbot/core/helpers/validators.py +++ b/bbot/core/helpers/validators.py @@ -129,14 +129,28 @@ def validate_host(host: Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address] raise ValidationError(f'Invalid hostname: "{host}"') +FINDING_SEVERITY_LEVELS = ("INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL") + + @validator def validate_severity(severity: str): severity = str(severity).strip().upper() - if severity not in ("UNKNOWN", "INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"): + if severity not in FINDING_SEVERITY_LEVELS: raise ValueError(f"Invalid severity: {severity}") return severity +FINDING_CONFIDENCE_LEVELS = ("UNKNOWN", "LOW", "MEDIUM", "HIGH", "CONFIRMED") + + +@validator +def validate_confidence(confidence: str): + confidence = str(confidence).strip().upper() + if confidence not in FINDING_CONFIDENCE_LEVELS: + raise ValueError(f"Invalid confidence: {confidence}") + return confidence + + @validator def validate_email(email: str): email = smart_encode_punycode(str(email).strip().lower()) diff --git a/bbot/core/helpers/web/client.py b/bbot/core/helpers/web/client.py index b76e6058ee..8fb0171bb0 100644 --- a/bbot/core/helpers/web/client.py +++ b/bbot/core/helpers/web/client.py @@ -70,7 +70,9 @@ def __init__(self, *args, **kwargs): cookies = {} # user agent - user_agent = self._web_config.get("user_agent", "BBOT") + user_agent = ( + f"{self._web_config.get('user_agent', 'BBOT')} {self._web_config.get('user_agent_suffix') or ''}".strip() + ) if "User-Agent" not in headers: headers["User-Agent"] = user_agent kwargs["headers"] = headers @@ -90,9 +92,9 @@ def build_request(self, *args, **kwargs): kwargs["url"] = url url = kwargs["url"] - target_in_scope = self._target.in_scope(str(url)) + in_target = self._target.in_target(str(url)) - if target_in_scope: + if in_target: if not kwargs.get("cookies", None): kwargs["cookies"] = {} for ck, cv in self._web_config.get("http_cookies", {}).items(): @@ -101,7 +103,7 @@ def build_request(self, *args, **kwargs): request = super().build_request(**kwargs) - if target_in_scope: + if in_target: for hk, hv in self._web_config.get("http_headers", {}).items(): hv = str(hv) # don't clobber headers diff --git a/bbot/core/helpers/web/web.py b/bbot/core/helpers/web/web.py index 60ff35dd59..9f8b751c65 100644 --- a/bbot/core/helpers/web/web.py +++ b/bbot/core/helpers/web/web.py @@ -56,10 +56,7 @@ def __init__(self, parent_helper): self.target = self.preset.target self.ssl_verify = self.config.get("ssl_verify", False) engine_debug = self.config.get("engine", {}).get("debug", False) - super().__init__( - server_kwargs={"config": self.config, "target": self.parent_helper.preset.target}, - debug=engine_debug, - ) + super().__init__(server_kwargs={"config": self.config, "target": self.target}, debug=engine_debug) def AsyncClient(self, *args, **kwargs): # cache by retries to prevent unwanted accumulation of clients @@ -359,7 +356,7 @@ async def curl(self, *args, **kwargs): log.debug("ignore_bbot_global_settings enabled. Global settings will not be applied") else: http_timeout = self.parent_helper.web_config.get("http_timeout", 20) - user_agent = self.parent_helper.web_config.get("user_agent", "BBOT") + user_agent = f"{self.parent_helper.web_config.get('user_agent', 'BBOT')} {self.parent_helper.web_config.get('user_agent_suffix') or ''}".strip() if "User-Agent" not in headers: headers["User-Agent"] = user_agent diff --git a/bbot/core/modules.py b/bbot/core/modules.py index 7bdb440b2d..1100919423 100644 --- a/bbot/core/modules.py +++ b/bbot/core/modules.py @@ -292,9 +292,8 @@ def preload_module(self, module_file): ], "flags": [ "active", - "safe", - "web-basic", - "web-thorough" + "web", + "web-heavy" ], "meta": { "description": "Extract technologies from web responses" @@ -331,7 +330,8 @@ def preload_module(self, module_file): config = {} options_desc = {} disable_auto_module_deps = False - python_code = open(module_file).read() + with open(module_file) as f: + python_code = f.read() # take a hash of the code so we can keep track of when it changes module_hash = sha1(python_code).hexdigest() parsed_code = ast.parse(python_code) @@ -556,12 +556,11 @@ def modules_table(self, modules=None, mod_type=None, include_author=False, inclu Examples: >>> print(modules_table(["portscan"])) - +----------+--------+-----------------+------------------------------+-------------------------------+----------------------+-------------------+ - | Module | Type | Needs API Key | Description | Flags | Consumed Events | Produced Events | - +==========+========+=================+==============================+===============================+======================+===================+ - | portscan | scan | No | Execute port scans | active, aggressive, portscan, | DNS_NAME, IP_ADDRESS | OPEN_TCP_PORT | - | | | | | web-thorough | | | - +----------+--------+-----------------+------------------------------+-------------------------------+----------------------+-------------------+ + +----------+--------+-----------------+------------------------------+------------------+----------------------+-------------------+ + | Module | Type | Needs API Key | Description | Flags | Consumed Events | Produced Events | + +==========+========+=================+==============================+==================+======================+===================+ + | portscan | scan | No | Execute port scans | active, portscan | DNS_NAME, IP_ADDRESS | OPEN_TCP_PORT | + +----------+--------+-----------------+------------------------------+------------------+----------------------+-------------------+ """ table = [] diff --git a/bbot/defaults.yml b/bbot/defaults.yml index 64614d08e1..a00adad9d9 100644 --- a/bbot/defaults.yml +++ b/bbot/defaults.yml @@ -1,5 +1,14 @@ ### BASIC OPTIONS ### +# NOTE: If used in a preset, these options must be nested underneath "config:" like so: +# config: +# home: ~/.bbot +# keep_scans: 20 +# scope: +# strict: true +# dns: +# minimal: true + # BBOT working directory home: ~/.bbot # How many scan results to keep before cleaning up the older ones @@ -15,7 +24,7 @@ folder_blobs: false scope: # strict scope means only exact DNS names are considered in-scope - # subdomains are not included unless they are explicitly provided in the target list + # their subdomains are not included unless explicitly added to the target strict: false # Filter by scope distance which events are displayed in the output # 0 == show only in-scope events (affiliates are always shown) @@ -28,7 +37,7 @@ scope: ### DNS ### dns: - # Completely disable DNS resolution (careful if you have IP whitelists/blacklists, consider using minimal=true instead) + # Completely disable DNS resolution (careful if you have IP targets/blacklists, consider using minimal=true instead) disable: false # Speed up scan by not creating any new DNS events, and only resolving A and AAAA records minimal: false @@ -59,7 +68,9 @@ dns: # Skip DNS requests for a certain domain and rdtype after encountering this many timeouts or SERVFAILs # This helps prevent faulty DNS servers from hanging up the scan abort_threshold: 50 - # Don't show PTR records containing IP addresses + # Treat hostnames discovered via PTR records as affiliates instead of in-scope + # This prevents rDNS results (e.g. 1-2-3-4.ptr.example.com) from triggering + # subdomain enumeration against unrelated domains when scanning IP ranges filter_ptrs: true # Enable/disable debug messages for DNS queries debug: false @@ -77,6 +88,8 @@ web: http_proxy: # Web user-agent user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.2151.97 + # Suffix to append to user-agent (e.g. for tracking or identification) + user_agent_suffix: # Set the maximum number of HTTP links that can be followed in a row (0 == no spidering allowed) spider_distance: 0 # Set the maximum directory depth for the web spider @@ -246,6 +259,7 @@ parameter_blacklist: - __SCROLLPOSITIONY - __SCROLLPOSITIONX - ASP.NET_SessionId + - .AspNetCore.Session - PHPSESSID - __cf_bm - f5_cspm @@ -253,6 +267,7 @@ parameter_blacklist: parameter_blacklist_prefixes: - TS01 - BIGipServer + - f5avr - incap_ - visid_incap_ - AWSALB diff --git a/bbot/models/helpers.py b/bbot/models/helpers.py new file mode 100644 index 0000000000..b94bc976cc --- /dev/null +++ b/bbot/models/helpers.py @@ -0,0 +1,20 @@ +from datetime import datetime +from zoneinfo import ZoneInfo + + +def utc_datetime_validator(d: datetime) -> datetime: + """ + Converts all dates into UTC + """ + if d.tzinfo is not None: + return d.astimezone(ZoneInfo("UTC")) + else: + return d.replace(tzinfo=ZoneInfo("UTC")) + + +def utc_now() -> datetime: + return datetime.now(ZoneInfo("UTC")) + + +def utc_now_timestamp() -> datetime: + return utc_now().timestamp() diff --git a/bbot/models/pydantic.py b/bbot/models/pydantic.py new file mode 100644 index 0000000000..6f6d624338 --- /dev/null +++ b/bbot/models/pydantic.py @@ -0,0 +1,159 @@ +import json +import logging +from pydantic import BaseModel, ConfigDict, Field, computed_field +from typing import Optional, List, Annotated, get_origin, get_args + +from bbot.models.helpers import utc_now_timestamp + +log = logging.getLogger("bbot_server.models") + + +class BBOTBaseModel(BaseModel): + model_config = ConfigDict(extra="ignore") + + def to_json(self, **kwargs): + return json.dumps(self.model_dump(), sort_keys=True, **kwargs) + + def __hash__(self): + return hash(self.to_json()) + + def __eq__(self, other): + return hash(self) == hash(other) + + @classmethod + def indexed_fields(cls): + indexed_fields = {} + + # Handle regular fields + for fieldname, field in cls.model_fields.items(): + if any(isinstance(m, str) and m.startswith("indexed") for m in field.metadata): + indexed_fields[fieldname] = field.metadata + + # Handle computed fields + for fieldname, field in cls.model_computed_fields.items(): + return_type = field.return_type + if get_origin(return_type) is Annotated: + type_args = get_args(return_type) + metadata = list(type_args[1:]) # Skip the first arg (the actual type) + if any(isinstance(m, str) and m.startswith("indexed") for m in metadata): + indexed_fields[fieldname] = metadata + + return indexed_fields + + # we keep these because they were a lot of work to make and maybe someday they'll be useful again + + # @classmethod + # def _get_type_hints(cls): + # """ + # Drills down past all the Annotated, Optional, and Union layers to get the underlying type hint + # """ + # type_hints = get_type_hints(cls) + # unwrapped_type_hints = {} + # for field_name in cls.model_fields: + # type_hint = type_hints[field_name] + # while 1: + # if getattr(type_hint, "__origin__", None) in (Annotated, Optional, Union): + # type_hint = type_hint.__args__[0] + # else: + # break + # unwrapped_type_hints[field_name] = type_hint + # return unwrapped_type_hints + + # @classmethod + # def _datetime_fields(cls): + # datetime_fields = [] + # for field_name, type_hint in cls._get_type_hints().items(): + # if type_hint == datetime: + # datetime_fields.append(field_name) + # return sorted(datetime_fields) + + +### EVENT ### + + +class Event(BBOTBaseModel): + uuid: Annotated[str, "indexed", "unique"] + id: Annotated[str, "indexed"] + type: Annotated[str, "indexed"] + scope_description: str + data: Annotated[Optional[str], "indexed"] = None + data_json: Optional[dict] = None + host: Annotated[Optional[str], "indexed"] = None + port: Optional[int] = None + netloc: Optional[str] = None + resolved_hosts: Optional[List] = None + dns_children: Optional[dict] = None + web_spider_distance: Optional[int] = None + scope_distance: int = 10 + scan: Annotated[str, "indexed"] + timestamp: Annotated[float, "indexed"] + inserted_at: Annotated[Optional[float], "indexed"] = Field(default_factory=utc_now_timestamp) + parent: Annotated[str, "indexed"] + parent_uuid: Annotated[str, "indexed"] + tags: List = [] + host_metadata: Optional[dict] = None + module: Annotated[Optional[str], "indexed"] = None + module_sequence: Optional[str] = None + discovery_context: str = "" + discovery_path: List[str] = [] + parent_chain: List[str] = [] + archived: bool = False + + def get_data(self): + if self.data is not None: + return self.data + return self.data_json + + def __hash__(self): + return hash(self.id) + + @computed_field + @property + def reverse_host(self) -> Annotated[Optional[str], "indexed"]: + """ + We store the host in reverse to allow for instant subdomain queries + This works because indexes are left-anchored, but we need to search starting from the right side + """ + if self.host: + return self.host[::-1] + return None + + +### SCAN ### + + +class Scan(BBOTBaseModel): + id: Annotated[str, "indexed", "unique"] + name: str + status: Annotated[str, "indexed"] + started_at: Annotated[float, "indexed"] + finished_at: Annotated[Optional[float], "indexed"] = None + duration_seconds: Optional[float] = None + duration: Optional[str] = None + target: dict + preset: dict + + @classmethod + def from_scan(cls, scan): + return cls( + id=scan.id, + name=scan.name, + status=scan.status, + started_at=scan.started_at, + ) + + +### TARGET ### + + +class Target(BBOTBaseModel): + name: str = "Default Target" + strict_scope: bool = False + target: List = [] + seeds: Optional[List] = None + blacklist: List = [] + hash: Annotated[str, "indexed", "unique"] + scope_hash: Annotated[str, "indexed"] + seed_hash: Annotated[str, "indexed"] + target_hash: Annotated[str, "indexed"] + blacklist_hash: Annotated[str, "indexed"] diff --git a/bbot/db/sql/models.py b/bbot/models/sql.py similarity index 77% rename from bbot/db/sql/models.py rename to bbot/models/sql.py index d6e7656108..a419f6a56e 100644 --- a/bbot/db/sql/models.py +++ b/bbot/models/sql.py @@ -3,13 +3,15 @@ import json import logging +from datetime import datetime from pydantic import ConfigDict from typing import List, Optional -from datetime import datetime, timezone from typing_extensions import Annotated from pydantic.functional_validators import AfterValidator from sqlmodel import inspect, Column, Field, SQLModel, JSON, String, DateTime as SQLADateTime +from bbot.models.helpers import utc_now_timestamp + log = logging.getLogger("bbot_server.models") @@ -27,14 +29,6 @@ def naive_datetime_validator(d: datetime): NaiveUTC = Annotated[datetime, AfterValidator(naive_datetime_validator)] -class CustomJSONEncoder(json.JSONEncoder): - def default(self, obj): - # handle datetime - if isinstance(obj, datetime): - return obj.isoformat() - return super().default(obj) - - class BBOTBaseModel(SQLModel): model_config = ConfigDict(extra="ignore") @@ -52,7 +46,7 @@ def validated(self): return self def to_json(self, **kwargs): - return json.dumps(self.validated.model_dump(), sort_keys=True, cls=CustomJSONEncoder, **kwargs) + return json.dumps(self.validated.model_dump(), sort_keys=True, **kwargs) @classmethod def _pk_column_names(cls): @@ -71,20 +65,13 @@ def __eq__(self, other): class Event(BBOTBaseModel, table=True): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - data = self._get_data(self.data, self.type) - self.data = {self.type: data} if self.host: self.reverse_host = self.host[::-1] def get_data(self): - return self._get_data(self.data, self.type) - - @staticmethod - def _get_data(data, type): - # handle SIEM-friendly format - if isinstance(data, dict) and list(data) == [type]: - return data[type] - return data + if self.data is not None: + return self.data + return self.data_json uuid: str = Field( primary_key=True, @@ -93,27 +80,29 @@ def _get_data(data, type): ) id: str = Field(index=True) type: str = Field(index=True) - scope_description: str - data: dict = Field(sa_type=JSON) + data: Optional[str] = Field(default=None, index=True) + data_json: Optional[dict] = Field(default=None, sa_type=JSON) host: Optional[str] port: Optional[int] netloc: Optional[str] + scope_description: str # store the host in reversed form for efficient lookups by domain reverse_host: Optional[str] = Field(default="", exclude=True, index=True) resolved_hosts: List = Field(default=[], sa_type=JSON) dns_children: dict = Field(default={}, sa_type=JSON) - web_spider_distance: int = 10 + web_spider_distance: Optional[int] = None scope_distance: int = Field(default=10, index=True) scan: str = Field(index=True) - timestamp: NaiveUTC = Field(index=True) + timestamp: float = Field(index=True) + inserted_at: float = Field(default_factory=utc_now_timestamp) parent: str = Field(index=True) tags: List = Field(default=[], sa_type=JSON) + host_metadata: dict = Field(default={}, sa_type=JSON) module: str = Field(index=True) module_sequence: str discovery_context: str = "" discovery_path: List[str] = Field(default=[], sa_type=JSON) parent_chain: List[str] = Field(default=[], sa_type=JSON) - inserted_at: NaiveUTC = Field(default_factory=lambda: datetime.now(timezone.utc)) ### SCAN ### @@ -137,11 +126,11 @@ class Scan(BBOTBaseModel, table=True): class Target(BBOTBaseModel, table=True): name: str = "Default Target" strict_scope: bool = False - seeds: List = Field(default=[], sa_type=JSON) - whitelist: List = Field(default=None, sa_type=JSON) + target: List = Field(default=[], sa_type=JSON) + seeds: Optional[List] = Field(default=None, sa_type=JSON) blacklist: List = Field(default=[], sa_type=JSON) hash: str = Field(sa_column=Column("hash", String(length=255), unique=True, primary_key=True, index=True)) scope_hash: str = Field(sa_column=Column("scope_hash", String(length=255), index=True)) - seed_hash: str = Field(sa_column=Column("seed_hashhash", String(length=255), index=True)) - whitelist_hash: str = Field(sa_column=Column("whitelist_hash", String(length=255), index=True)) + seed_hash: str = Field(sa_column=Column("seed_hash", String(length=255), index=True)) + target_hash: str = Field(sa_column=Column("target_hash", String(length=255), index=True)) blacklist_hash: str = Field(sa_column=Column("blacklist_hash", String(length=255), index=True)) diff --git a/bbot/modules/ajaxpro.py b/bbot/modules/ajaxpro.py index 1df424ebcc..bdbebdab52 100644 --- a/bbot/modules/ajaxpro.py +++ b/bbot/modules/ajaxpro.py @@ -10,8 +10,8 @@ class ajaxpro(BaseModule): ajaxpro_regex = re.compile(r' 0: + branding = branding_info[0] + if isinstance(branding, dict): + branding_data = {} + if "BannerLogo" in branding: + branding_data["banner-logo"] = branding["BannerLogo"] + if "TileLogo" in branding: + branding_data["tile-logo"] = branding["TileLogo"] + if "BackgroundColor" in branding: + branding_data["background-color"] = branding["BackgroundColor"] + if branding_data: + result["tenant-branding"] = branding_data + + return result + + except Exception as e: + self.debug(f"Error querying UserRealm v2.0 endpoint for {domain}: {e}") + return {} + + async def query_mtasts(self, domain): + """Query MTA-STS policy to detect Exchange Online.""" + url = f"https://mta-sts.{domain}/.well-known/mta-sts.txt" + + try: + r = await self.helpers.request(url) + if r is None: + self.debug(f"No response from MTA-STS endpoint for {domain}") + return {} + + status_code = getattr(r, "status_code", 0) + if status_code == 404: + self.debug(f"No MTA-STS policy found for {domain}") + return {} + if status_code != 200: + self.debug(f"MTA-STS endpoint returned status {status_code} for {domain}") + return {} + + content = r.text + if not content: + return {} + + # Check if Exchange Online is in the MX records + result = {} + if "mail.protection.outlook.com" in content.lower(): + result["exchange-online"] = True + result["mta-sts-mode"] = None + + # Extract mode if present + for line in content.split("\n"): + if line.startswith("mode:"): + result["mta-sts-mode"] = line.split(":", 1)[1].strip() + break + + return result + + except Exception as e: + self.debug(f"Error querying MTA-STS endpoint for {domain}: {e}") + return {} + + async def query_directory_sync(self, onmicrosoft_domain): + """Check if directory sync is enabled by testing for sync service account.""" + if not onmicrosoft_domain: + return {} + + url = "https://login.microsoftonline.com/common/GetCredentialType" + headers = {"Content-Type": "application/json; charset=UTF-8"} + + sync_username = f"ADToAADSyncServiceAccount@{onmicrosoft_domain}" + body = { + "username": sync_username, + "isOtherIdpSupported": True, + "checkPhones": False, + "isRemoteNGCSupported": False, + "isCookieBannerShown": False, + "isFidoSupported": False, + "isAccessPassSupported": False, + } + + try: + r = await self.helpers.request(url, method="POST", headers=headers, json=body) + if r is None: + self.debug(f"No response from GetCredentialType for directory sync check on {onmicrosoft_domain}") + return {} + + status_code = getattr(r, "status_code", 0) + if status_code != 200: + self.debug(f"GetCredentialType returned status {status_code} for directory sync check") + return {} + + data = r.json() + if not data or not isinstance(data, dict): + return {} + + # If IfExistsResult is 0, the sync account exists + if_exists_result = data.get("IfExistsResult") + if if_exists_result == 0: + return {"directory-sync-enabled": True} + + return {} + + except Exception as e: + self.debug(f"Error checking directory sync for {onmicrosoft_domain}: {e}") + return {} + + def merge_tenant_data( + self, query, tenant_data, odc_data, openid_data, getcred_data, userrealm_data, mtasts_data, sync_data + ): + """Merge data from all sources into single comprehensive event.""" + + # Start with azmap.dev data (backward compatibility) + tenant_names = [] + tenant_name = tenant_data.get("tenant_name") + if tenant_name: + tenant_names.append(tenant_name) + + # Extract tenant names from .onmicrosoft.com domains + email_domains = tenant_data.get("email_domains", []) + for domain in email_domains: + if domain.lower().endswith(".onmicrosoft.com"): + tenantname = domain.split(".")[0].lower() + if tenantname and tenantname not in tenant_names: + tenant_names.append(tenantname) + + merged = {"tenant-names": tenant_names, "domains": sorted(email_domains)} + + # Tenant ID: prefer ODC, fallback to azmap.dev + tenant_id = odc_data.get("tenant-id") or tenant_data.get("tenant_id") + if tenant_id: + merged["tenant-id"] = tenant_id + + # Federation brand name from ODC + if odc_data.get("federation-brand-name"): + merged["federation-brand-name"] = odc_data["federation-brand-name"] + + # OpenID configuration data + if openid_data.get("tenant-region"): + merged["tenant-region"] = openid_data["tenant-region"] + if openid_data.get("tenant-region-sub") is not None: + merged["tenant-region-sub"] = openid_data["tenant-region-sub"] + if openid_data.get("cloud-instance"): + merged["cloud-instance"] = openid_data["cloud-instance"] + if openid_data: + merged["cloud-type"] = self._derive_cloud_type(openid_data) + + # GetCredentialType data + if getcred_data: + if getcred_data.get("desktop-sso-enabled") is not None: + merged["desktop-sso-enabled"] = getcred_data["desktop-sso-enabled"] + if getcred_data.get("certificate-auth-enabled") is not None: + merged["certificate-auth-enabled"] = getcred_data["certificate-auth-enabled"] + if getcred_data.get("domain-type") is not None: + merged["domain-type"] = getcred_data["domain-type"] + if getcred_data.get("federation-redirect-url"): + merged["federation-redirect-url"] = getcred_data["federation-redirect-url"] + + # UserRealm v2.0 data + if userrealm_data: + if userrealm_data.get("account-type"): + merged["account-type"] = userrealm_data["account-type"] + if userrealm_data.get("tenant-branding"): + merged["tenant-branding"] = userrealm_data["tenant-branding"] + + # MTA-STS data + if mtasts_data.get("exchange-online") is not None: + merged["exchange-online"] = mtasts_data["exchange-online"] + if mtasts_data.get("mta-sts-mode"): + merged["mta-sts-mode"] = mtasts_data["mta-sts-mode"] + + # Directory sync data + if sync_data and sync_data.get("directory-sync-enabled"): + merged["directory-sync-enabled"] = True + + return merged + + async def emit_discovered_domains(self, event, tenant_data, query): + """Emit DNS_NAME events for discovered domains (existing functionality).""" + email_domains = tenant_data.get("email_domains", []) + if email_domains: + self.verbose( + f'Found {len(email_domains):,} domains under tenant for "{query}": {", ".join(sorted(email_domains))}' + ) + for domain in email_domains: + if domain != query: + await self.emit_event( + domain, + "DNS_NAME", + parent=event, + tags=["affiliate", "azure-tenant"], + context=f'{{module}} queried azmap.dev for "{query}" and found {{event.type}}: {{event.pretty_string}}', + ) + + async def emit_findings(self, event, domain, tenant_data): + """Emit FINDING events for notable security-relevant discoveries.""" + + # Desktop SSO + if tenant_data.get("desktop-sso-enabled"): + await self.emit_event( + { + "host": domain, + "name": "Azure AD Desktop SSO Enabled", + "description": f"Azure AD Desktop SSO (Seamless SSO) is enabled for {domain}, indicating pass-through authentication or password hash sync from on-premises Active Directory. This reveals hybrid infrastructure where compromising the on-premises environment could provide a pivot point into Azure AD.", + "severity": "INFO", + "confidence": "MEDIUM", + }, + "FINDING", + parent=event, + tags=["azure-sso"], + ) + + # Certificate-based Auth + if tenant_data.get("certificate-auth-enabled"): + await self.emit_event( + { + "host": domain, + "name": "Certificate-Based Authentication Enabled", + "description": f"Certificate-based authentication is enabled for {domain}. This provides an alternative authentication vector where stolen or compromised certificates can be used for access, potentially bypassing MFA and password-based security controls.", + "severity": "INFO", + "confidence": "HIGH", + }, + "FINDING", + parent=event, + tags=["azure-cba"], + ) + + # Government Cloud + cloud_type = tenant_data.get("cloud-type") + if cloud_type in ["gcc-high", "dod"]: + await self.emit_event( + { + "host": domain, + "name": "Azure Government Cloud Tenant", + "description": f"{domain} is hosted on Azure Government Cloud ({cloud_type}), indicating a high-value target that likely handles sensitive government data, classified information, or critical infrastructure systems.", + "severity": "INFO", + "confidence": "HIGH", + }, + "FINDING", + parent=event, + tags=["azure-gov-cloud"], + ) + + # Directory Sync + if tenant_data.get("directory-sync-enabled"): + await self.emit_event( + { + "host": domain, + "name": "Directory Synchronization Enabled", + "description": f"Directory synchronization (Azure AD Connect or Cloud Sync) is enabled for {domain}, indicating hybrid identity infrastructure. Compromising the on-premises Active Directory would allow an attacker to sync malicious changes to Azure AD, including creating backdoor accounts.", + "severity": "INFO", + "confidence": "HIGH", + }, + "FINDING", + parent=event, + tags=["azure-dir-sync"], + ) + + # Federated Domain + fed_url = tenant_data.get("federation-redirect-url") + if fed_url: + await self.emit_event( + { + "host": domain, + "url": fed_url, + "full_url": fed_url, # Preserve full URL with query string + "name": "Federated Authentication Detected", + "description": f"{domain} uses federated authentication with an external identity provider at {fed_url}. Compromising the federated IdP would grant access to Azure AD, and misconfigurations in the federation trust can be exploited (e.g., Golden SAML attacks).", + "severity": "INFO", + "confidence": "HIGH", + }, + "FINDING", + parent=event, + tags=["azure-federated"], + ) + # Also emit URL for further enumeration (ms-auth-url tag triggers oauth module OIDC check) + fed_url_event = self.make_event( + fed_url, + "URL_UNVERIFIED", + parent=event, + tags=["affiliate", "ms-auth-url"], + ) + if fed_url_event: + await self.emit_event(fed_url_event) + + def _derive_cloud_type(self, openid_data): + """Derive cloud type from OpenID configuration data.""" + sub_scope = openid_data.get("tenant-region-sub") + + if sub_scope is None: + return "commercial" + elif sub_scope == "DOD": + return "dod" + elif sub_scope == "DODCON": + return "gcc-high" + else: + return "government" + + def _get_onmicrosoft_domain(self, tenant_data): + """Extract the .onmicrosoft.com domain from tenant data.""" + email_domains = tenant_data.get("email_domains", []) + for domain in email_domains: + if domain.lower().endswith(".onmicrosoft.com"): + return domain + return None diff --git a/bbot/modules/baddns.py b/bbot/modules/baddns.py index 7e91cddee6..ace72c9c68 100644 --- a/bbot/modules/baddns.py +++ b/bbot/modules/baddns.py @@ -2,27 +2,58 @@ from baddns.lib.loader import load_signatures from .base import BaseModule -import asyncio import logging +SEVERITY_LEVELS = ("INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL") +CONFIDENCE_LEVELS = ("UNKNOWN", "LOW", "MODERATE", "HIGH", "CONFIRMED") + +SUBMODULE_MAX_SEVERITY = { + "CNAME": "MEDIUM", + "NS": "MEDIUM", + "MX": "MEDIUM", + "TXT": "LOW", + "references": "MEDIUM", + "NSEC": "INFO", + "zonetransfer": "INFO", + "DMARC": "INFO", + "SPF": "MEDIUM", + "MTA-STS": "HIGH", + "WILDCARD": "HIGH", +} + +SUBMODULE_MAX_CONFIDENCE = { + "CNAME": "CONFIRMED", + "NS": "HIGH", + "MX": "CONFIRMED", + "TXT": "CONFIRMED", + "references": "CONFIRMED", + "NSEC": "CONFIRMED", + "zonetransfer": "CONFIRMED", + "DMARC": "CONFIRMED", + "SPF": "CONFIRMED", + "MTA-STS": "CONFIRMED", + "WILDCARD": "CONFIRMED", +} + class baddns(BaseModule): watched_events = ["DNS_NAME", "DNS_NAME_UNRESOLVED"] - produced_events = ["FINDING", "VULNERABILITY"] - flags = ["active", "safe", "web-basic", "baddns", "cloud-enum", "subdomain-hijack"] + produced_events = ["FINDING"] + flags = ["safe", "active", "web", "baddns", "cloud-enum", "subdomain-hijack"] meta = { "description": "Check hosts for domain/subdomain takeovers", "created_date": "2024-01-18", "author": "@liquidsec", } - options = {"custom_nameservers": [], "only_high_confidence": False, "enabled_submodules": []} + options = {"custom_nameservers": [], "min_severity": "LOW", "min_confidence": "MODERATE", "enabled_submodules": []} options_desc = { "custom_nameservers": "Force BadDNS to use a list of custom nameservers", - "only_high_confidence": "Do not emit low-confidence or generic detections", + "min_severity": "Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL)", + "min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED)", "enabled_submodules": "A list of submodules to enable. Empty list (default) enables CNAME, TXT and MX Only", } module_threads = 8 - deps_pip = ["baddns~=1.12.294"] + deps_pip = ["baddns~=2.0.0"] def select_modules(self): selected_submodules = [] @@ -36,14 +67,50 @@ def set_modules(self): if self.enabled_submodules == []: self.enabled_submodules = ["CNAME", "MX", "TXT"] + def _filter_submodules(self): + filtered = [] + for name in self.enabled_submodules: + max_sev = SUBMODULE_MAX_SEVERITY.get(name) + max_conf = SUBMODULE_MAX_CONFIDENCE.get(name) + if max_sev is None or max_conf is None: + filtered.append(name) + continue + sev_idx = SEVERITY_LEVELS.index(max_sev) if max_sev in SEVERITY_LEVELS else 0 + conf_idx = CONFIDENCE_LEVELS.index(max_conf) if max_conf in CONFIDENCE_LEVELS else 0 + if sev_idx < self._min_sev_idx or conf_idx < self._min_conf_idx: + self.verbose( + f"Auto-disabling submodule [{name}]: max_severity={max_sev}, max_confidence={max_conf} below configured thresholds" + ) + else: + filtered.append(name) + return filtered + + def _meets_threshold(self, severity, confidence): + sev_idx = SEVERITY_LEVELS.index(severity) if severity in SEVERITY_LEVELS else 0 + conf_idx = CONFIDENCE_LEVELS.index(confidence) if confidence in CONFIDENCE_LEVELS else 0 + return sev_idx >= self._min_sev_idx and conf_idx >= self._min_conf_idx + async def setup(self): self.preset.core.logger.include_logger(logging.getLogger("baddns")) self.custom_nameservers = self.config.get("custom_nameservers", []) or None if self.custom_nameservers: self.custom_nameservers = self.helpers.chain_lists(self.custom_nameservers) - self.only_high_confidence = self.config.get("only_high_confidence", False) + min_severity = self.config.get("min_severity", "LOW").upper() + min_confidence = self.config.get("min_confidence", "MODERATE").upper() + if min_severity not in SEVERITY_LEVELS: + self.warning(f"Invalid min_severity: {min_severity}, defaulting to LOW") + min_severity = "LOW" + if min_confidence not in CONFIDENCE_LEVELS: + self.warning(f"Invalid min_confidence: {min_confidence}, defaulting to MODERATE") + min_confidence = "MODERATE" + self._min_sev_idx = SEVERITY_LEVELS.index(min_severity) + self._min_conf_idx = CONFIDENCE_LEVELS.index(min_confidence) self.signatures = load_signatures() self.set_modules() + self.enabled_submodules = self._filter_submodules() + if not self.enabled_submodules: + self.warning("All submodules were disabled by severity/confidence thresholds") + return False all_submodules_list = [m.name for m in get_all_modules()] for m in self.enabled_submodules: if m not in all_submodules_list: @@ -54,11 +121,34 @@ async def setup(self): self.debug(f"Enabled BadDNS Submodules: [{','.join(self.enabled_submodules)}]") return True + async def _run_module(self, module_instance): + """Wrapper coroutine that runs a module and returns both the module and result""" + try: + result = await module_instance.dispatch() + return module_instance, result + except Exception as e: + self.warning(f"Task for {module_instance} raised an error: {e}") + return module_instance, None + + def _new_http_client(self, *args, **kwargs): + """Create a non-cached HTTP client for baddns submodules. + + baddns submodules close their HTTP clients during cleanup, so we can't + use the caching ``web.AsyncClient`` factory — that would let one + submodule close a client that another submodule is still using. + + TODO: revisit this when we switch to blasthttp — the caching/lifecycle + model will be different and this workaround may no longer be needed. + """ + from bbot.core.helpers.web.client import BBOTAsyncClient + + return BBOTAsyncClient.from_config(self.scan.config, self.scan.target, *args, persist_cookies=False, **kwargs) + async def handle_event(self, event): - tasks = [] + coroutines = [] for ModuleClass in self.select_modules(): kwargs = { - "http_client_class": self.scan.helpers.web.AsyncClient, + "http_client_class": self._new_http_client, "dns_client": self.scan.helpers.dns.resolver, "custom_nameservers": self.custom_nameservers, "signatures": self.signatures, @@ -70,16 +160,16 @@ async def handle_event(self, event): kwargs["raw_query_retry_wait"] = 0 module_instance = ModuleClass(event.data, **kwargs) - task = asyncio.create_task(module_instance.dispatch()) - tasks.append((module_instance, task)) + # Create wrapper coroutine that includes the module instance + coroutine = self._run_module(module_instance) + coroutines.append(coroutine) - async for completed_task in self.helpers.as_completed([task for _, task in tasks]): - module_instance = next((m for m, t in tasks if t == completed_task), None) + async for completed_coro in self.helpers.as_completed(coroutines): try: - task_result = await completed_task + module_instance, task_result = await completed_coro except Exception as e: - self.warning(f"Task for {module_instance} raised an error: {e}") - task_result = None + self.warning(f"Wrapper coroutine raised an error: {e}") + continue if task_result: results = module_instance.analyze() @@ -88,41 +178,28 @@ async def handle_event(self, event): r_dict = r.to_dict() confidence = r_dict["confidence"] + severity = r_dict["severity"] - if confidence in ["CONFIRMED", "PROBABLE"]: - data = { - "severity": "MEDIUM", - "description": f"{r_dict['description']}. Confidence: [{confidence}] Signature: [{r_dict['signature']}] Indicator: [{r_dict['indicator']}] Trigger: [{r_dict['trigger']}] baddns Module: [{r_dict['module']}]", - "host": str(event.host), - } - await self.emit_event( - data, - "VULNERABILITY", - event, - tags=[f"baddns-{module_instance.name.lower()}"], - context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {r_dict["description"]}', + if not self._meets_threshold(severity, confidence): + self.debug( + f"Skipping result below threshold (severity={severity}, confidence={confidence})" ) - - elif confidence in ["UNLIKELY", "POSSIBLE"]: - if not self.only_high_confidence: - data = { - "description": f"{r_dict['description']} Confidence: [{confidence}] Signature: [{r_dict['signature']}] Indicator: [{r_dict['indicator']}] Trigger: [{r_dict['trigger']}] baddns Module: [{r_dict['module']}]", - "host": str(event.host), - } - await self.emit_event( - data, - "FINDING", - event, - tags=[f"baddns-{module_instance.name.lower()}"], - context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {r_dict["description"]}', - ) - else: - self.debug( - f"Skipping low-confidence result due to only_high_confidence setting: {confidence}" - ) - - else: - self.warning(f"Got unrecognized confidence level: {confidence}") + continue + + data = { + "severity": severity, + "name": f"BadDNS {r_dict['signature']}", + "confidence": confidence, + "description": f"{r_dict['description']}. Confidence: [{confidence}] Signature: [{r_dict['signature']}] Indicator: [{r_dict['indicator']}] Trigger: [{r_dict['trigger']}] baddns Module: [{r_dict['module']}]", + "host": str(event.host), + } + await self.emit_event( + data, + "FINDING", + event, + tags=[f"baddns-{module_instance.name.lower()}"], + context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {r_dict["description"]}', + ) found_domains = r_dict.get("found_domains", None) if found_domains: @@ -132,6 +209,6 @@ async def handle_event(self, event): "DNS_NAME", event, tags=[f"baddns-{module_instance.name.lower()}"], - context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {{event.data}}', + context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {{event.pretty_string}}', ) - await module_instance.cleanup() + await module_instance.cleanup() diff --git a/bbot/modules/baddns_direct.py b/bbot/modules/baddns_direct.py index 23fd9882c5..f4093fa8cc 100644 --- a/bbot/modules/baddns_direct.py +++ b/bbot/modules/baddns_direct.py @@ -1,43 +1,28 @@ -from baddns.base import get_all_modules -from baddns.lib.loader import load_signatures -from .base import BaseModule +from .baddns import baddns as baddns_module -import logging - -class baddns_direct(BaseModule): +class baddns_direct(baddns_module): watched_events = ["URL", "STORAGE_BUCKET"] - produced_events = ["FINDING", "VULNERABILITY"] - flags = ["active", "safe", "subdomain-enum", "baddns", "cloud-enum"] + produced_events = ["FINDING"] + flags = ["safe", "active", "subdomain-enum", "baddns", "cloud-enum"] meta = { "description": "Check for unusual subdomain / service takeover edge cases that require direct detection", "created_date": "2024-01-29", "author": "@liquidsec", } - options = {"custom_nameservers": []} + options = {"custom_nameservers": [], "min_severity": "LOW", "min_confidence": "MODERATE"} options_desc = { "custom_nameservers": "Force BadDNS to use a list of custom nameservers", + "min_severity": "Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL)", + "min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED)", } module_threads = 8 - deps_pip = ["baddns~=1.12.294"] + deps_pip = ["baddns~=2.0.0"] scope_distance_modifier = 1 - async def setup(self): - self.preset.core.logger.include_logger(logging.getLogger("baddns")) - self.custom_nameservers = self.config.get("custom_nameservers", []) or None - if self.custom_nameservers: - self.custom_nameservers = self.helpers.chain_lists(self.custom_nameservers) - self.only_high_confidence = self.config.get("only_high_confidence", False) - self.signatures = load_signatures() - return True - - def select_modules(self): - selected_modules = [] - for m in get_all_modules(): - if m.name in ["CNAME"]: - selected_modules.append(m) - return selected_modules + def set_modules(self): + self.enabled_submodules = ["CNAME"] async def handle_event(self, event): CNAME_direct_module = self.select_modules()[0] @@ -56,9 +41,19 @@ async def handle_event(self, event): for r in results: r_dict = r.to_dict() + severity = r_dict["severity"] + confidence = r_dict["confidence"] + + if not self._meets_threshold(severity, confidence): + self.debug(f"Skipping result below threshold (severity={severity}, confidence={confidence})") + continue + data = { + "name": f"BadDNS {r_dict['signature']}", "description": f"Possible [{r_dict['signature']}] via direct BadDNS analysis. Indicator: [{r_dict['indicator']}] Trigger: [{r_dict['trigger']}] baddns Module: [{r_dict['module']}]", "host": str(event.host), + "severity": severity, + "confidence": confidence, } await self.emit_event( @@ -81,8 +76,8 @@ async def filter_event(self, event): f"Rejecting {event.host} due to not being in scope (scope distance: {event.scope_distance})" ) return False - if "cdn-cloudflare" not in event.tags: - self.debug(f"Rejecting {event.host} due to not being behind CloudFlare") + if "cloudflare" not in event.tags: + self.debug(f"Rejecting {event.host} due to not being behind Cloudflare") return False if "status-200" in event.tags or "status-301" in event.tags: self.debug(f"Rejecting {event.host} due to lack of non-standard status code") diff --git a/bbot/modules/baddns_zone.py b/bbot/modules/baddns_zone.py index 6ee7662307..3f81906395 100644 --- a/bbot/modules/baddns_zone.py +++ b/bbot/modules/baddns_zone.py @@ -3,20 +3,21 @@ class baddns_zone(baddns_module): watched_events = ["DNS_NAME"] - produced_events = ["FINDING", "VULNERABILITY"] - flags = ["active", "safe", "subdomain-enum", "baddns", "cloud-enum"] + produced_events = ["FINDING"] + flags = ["safe", "active", "subdomain-enum", "baddns", "cloud-enum"] meta = { "description": "Check hosts for DNS zone transfers and NSEC walks", "created_date": "2024-01-29", "author": "@liquidsec", } - options = {"custom_nameservers": [], "only_high_confidence": False} + options = {"custom_nameservers": [], "min_severity": "INFO", "min_confidence": "MODERATE"} options_desc = { "custom_nameservers": "Force BadDNS to use a list of custom nameservers", - "only_high_confidence": "Do not emit low-confidence or generic detections", + "min_severity": "Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL)", + "min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED)", } module_threads = 8 - deps_pip = ["baddns~=1.12.294"] + deps_pip = ["baddns~=2.0.0"] def set_modules(self): self.enabled_submodules = ["NSEC", "zonetransfer"] diff --git a/bbot/modules/badsecrets.py b/bbot/modules/badsecrets.py index 26f9f5a09d..a5f8049f9e 100644 --- a/bbot/modules/badsecrets.py +++ b/bbot/modules/badsecrets.py @@ -6,8 +6,8 @@ class badsecrets(BaseModule): watched_events = ["HTTP_RESPONSE"] - produced_events = ["FINDING", "VULNERABILITY", "TECHNOLOGY"] - flags = ["active", "safe", "web-basic"] + produced_events = ["FINDING", "TECHNOLOGY"] + flags = ["safe", "active", "web"] meta = { "description": "Library for detecting known or weak secrets across many web frameworks", "created_date": "2022-11-19", @@ -17,7 +17,7 @@ class badsecrets(BaseModule): options_desc = { "custom_secrets": "Include custom secrets loaded from a local file", } - deps_pip = ["badsecrets~=0.13.47"] + deps_pip = ["badsecrets~=1.0.0"] async def setup(self): self.custom_secrets = None @@ -58,7 +58,7 @@ async def handle_event(self, event): body=resp_body, headers=resp_headers, cookies=resp_cookies, - url=event.data.get("url", None), + url=event.url or None, custom_resource=self.custom_secrets, ) except Exception as e: @@ -69,31 +69,40 @@ async def handle_event(self, event): if r["type"] == "SecretFound": data = { "severity": r["description"]["severity"], + "name": "BadSecrets - Known Secret", "description": f"Known Secret Found. Secret Type: [{r['description']['secret']}] Secret: [{r['secret']}] Product Type: [{r['description']['product']}] Product: [{self.helpers.truncate_string(r['product'], 2000)}] Detecting Module: [{r['detecting_module']}] Details: [{r['details']}]", - "url": event.data["url"], + "url": event.url, "host": str(event.host), + "confidence": "CONFIRMED", } await self.emit_event( data, - "VULNERABILITY", + "FINDING", event, context=f'{{module}}\'s "{r["detecting_module"]}" module found known {r["description"]["product"]} secret ({{event.type}}): "{r["secret"]}"', ) elif r["type"] == "IdentifyOnly": - # There is little value to presenting a non-vulnerable asp.net viewstate, as it is not crackable without a Matrioshka brain. Just emit a technology instead. - if r["detecting_module"] == "ASPNET_Viewstate": + # Excavate's JWTExtractor submodule already emits findings for JWT existence. + # Only vulnerable (SecretFound) JWT results are worth emitting from badsecrets. + if r["detecting_module"] == "Generic_JWT": + continue + # There is little value to presenting a non-vulnerable asp.net viewstate/resource, as it is not crackable without a Matrioshka brain. Just emit a technology instead. + if r["detecting_module"] in ("ASPNET_Viewstate", "ASPNET_Resource"): technology = "microsoft asp.net" await self.emit_event( - {"technology": technology, "url": event.data["url"], "host": str(event.host)}, + {"technology": technology, "url": event.url, "host": str(event.host)}, "TECHNOLOGY", event, context=f"{{module}} identified {{event.type}}: {technology}", ) else: data = { + "name": "BadSecrets - Cryptographic Product", "description": f"Cryptographic Product identified. Product Type: [{r['description']['product']}] Product: [{self.helpers.truncate_string(r['product'], 2000)}] Detecting Module: [{r['detecting_module']}]", - "url": event.data["url"], + "url": event.url, "host": str(event.host), + "severity": "INFO", + "confidence": "CONFIRMED", } await self.emit_event( data, diff --git a/bbot/modules/base.py b/bbot/modules/base.py index eafc193277..d7356bd809 100644 --- a/bbot/modules/base.py +++ b/bbot/modules/base.py @@ -20,7 +20,7 @@ class BaseModule: meta (Dict): Metadata about the module, such as whether authentication is required and a description. - flags (List): Flags indicating the type of module (must have at least "safe" or "aggressive" and "passive" or "active"). + flags (List): Flags indicating the type of module (must have at least "passive" or "active"). deps_modules (List): Other BBOT modules this module depends on. Empty list by default. @@ -52,6 +52,9 @@ class BaseModule: target_only (bool): Accept only the initial target event(s). Default is False. + accept_seeds (bool): Accept seed events (events from initial scan seeds). + Defaults to True for passive modules, False otherwise. Can be explicitly set to override the default. + in_scope_only (bool): Accept only explicitly in-scope events, regardless of the scan's search distance. Default is False. accept_url_special (bool): Accept "special" URLs not typically distributed to web modules, e.g. JS URLs. Default is False. @@ -111,7 +114,7 @@ class BaseModule: # whether to retry on 429s when first pinging the API at scan start _ping_retry_on_http_429 = False - default_discovery_context = "{module} discovered {event.type}: {event.data}" + default_discovery_context = "{module} discovered {event.type}: {event.pretty_string}" _preserve_graph = False _stats_exclude = False @@ -491,6 +494,9 @@ async def _handle_batch(self): await self.run_task(self.handle_batch(*events), context, n=len(events)) except asyncio.CancelledError: self.debug(f"{context} was cancelled") + finally: + for event in events: + event._minimize() self.verbose(f"Finished handling batch of {len(events):,} events") if finish: context = f"{self.name}.finish()" @@ -660,11 +666,14 @@ async def _events_waiting(self, batch_size=None): if acceptable: if event.type == "FINISHED": finish = True + event._minimize() else: events.append(event) self.scan.stats.event_consumed(event, self) - elif reason: - self.debug(f"Not accepting {event} because {reason}") + else: + event._minimize() + if reason: + self.debug(f"Not accepting {event} because {reason}") except asyncio.queues.QueueEmpty: break return events, finish @@ -738,8 +747,8 @@ async def _worker(self): - Each event is subject to a post-check via '_event_postcheck()' to decide whether it should be handled. - Special 'FINISHED' events trigger the 'finish()' method of the module. """ - async with self.scan._acatch(context=self._worker, unhandled_is_critical=True): - try: + try: + async with self.scan._acatch(context=self._worker, unhandled_is_critical=True): while not self.scan.stopping and not self.errored: # if batch wasn't big enough, we wait for the next event before continuing if self.batch_size > 1: @@ -758,40 +767,54 @@ async def _worker(self): except asyncio.queues.QueueEmpty: continue self.debug(f"Got {event} from {getattr(event, 'module', 'unknown_module')}") - async with self._task_counter.count(f"event_postcheck({event})"): - acceptable, reason = await self._event_postcheck(event) - if acceptable: - if event.type == "FINISHED": - context = f"{self.name}.finish()" - try: - await self.run_task(self.finish(), context) - except asyncio.CancelledError: - self.debug(f"{context} was cancelled") - continue + try: + async with self._task_counter.count(f"event_postcheck({event})"): + acceptable, reason = await self._event_postcheck(event) + if acceptable: + if event.type == "FINISHED": + context = f"{self.name}.finish()" + try: + await self.run_task(self.finish(), context) + except asyncio.CancelledError: + self.debug(f"{context} was cancelled") + continue + else: + context = f"{self.name}.handle_event({event})" + self.scan.stats.event_consumed(event, self) + self.debug(f"Handling {event}") + try: + await self.run_task(self.handle_event(event), context) + except asyncio.CancelledError: + self.debug(f"{context} was cancelled") + continue + self.debug(f"Finished handling {event}") else: - context = f"{self.name}.handle_event({event})" - self.scan.stats.event_consumed(event, self) - self.debug(f"Handling {event}") - try: - await self.run_task(self.handle_event(event), context) - except asyncio.CancelledError: - self.debug(f"{context} was cancelled") - continue - self.debug(f"Finished handling {event}") - else: - self.debug(f"Not accepting {event} because {reason}") - except asyncio.CancelledError: - # this trace was used for debugging leaked CancelledErrors from inside httpx - # self.log.trace("Worker cancelled") - raise - except BaseException as e: - if self.helpers.in_exception_chain(e, (KeyboardInterrupt,)): - self.scan.stop() - else: - self.error(f"Critical failure in module {self.name}: {e}") - self.error(traceback.format_exc()) + self.debug(f"Not accepting {event} because {reason}") + finally: + event._minimize() + except asyncio.CancelledError: + # this trace was used for debugging leaked CancelledErrors from inside httpx + # self.log.trace("Worker cancelled") + raise + except RuntimeError as e: + self.trace(f"RuntimeError in module {self.name}: {e}") + except BaseException as e: + if self.helpers.in_exception_chain(e, (KeyboardInterrupt,)): + await self.scan.async_stop() + else: + self.error(f"Critical failure in module {self.name}: {e}") + self.error(traceback.format_exc()) self.log.trace("Worker stopped") + @property + def accept_seeds(self): + """ + Returns whether the module accepts seed events. + Defaults to True for passive modules, False otherwise. + """ + # Default to True for passive modules, False otherwise + return "passive" in self.flags + @property def max_scope_distance(self): """ @@ -838,11 +861,15 @@ def _event_precheck(self, event): if self.errored: return False, "module is in error state" # exclude non-watched types - if not any(t in self.get_watched_events() for t in ("*", event.type)): + watched_events = self.get_watched_events() + event_type_watched = any(t in watched_events for t in ("*", event.type)) + # Check if module accepts seeds and event is a seed (only if event type is watched) + if self.accept_seeds and "seed" in event.tags and event_type_watched: + return True, "it is a seed event and module accepts seeds" + if not event_type_watched: return False, "its type is not in watched_events" - if self.target_only: - if "target" not in event.tags: - return False, "it did not meet target_only filter criteria" + if self.target_only and "target" not in event.tags: + return False, "it did not meet target_only filter criteria" # limit js URLs to modules that opt in to receive them if (not self.accept_url_special) and event.type.startswith("URL"): @@ -917,6 +944,9 @@ async def _event_postcheck_inner(self, event): return True, "" def _scope_distance_check(self, event): + # Seeds bypass scope distance checks + if self.accept_seeds and "seed" in event.tags: + return True, "it is a seed event and module accepts seeds" if self.in_scope_only: if event.scope_distance > 0: return False, "it did not meet in_scope_only filter criteria" @@ -1001,6 +1031,7 @@ async def queue_event(self, event): self.debug(f"Queueing {event} because {reason}") try: self.incoming_event_queue.put_nowait(event) + event._module_consumers += 1 async with self.event_received: self.event_received.notify() if event.type != "FINISHED": @@ -1803,8 +1834,8 @@ class BaseInterceptModule(BaseModule): _intercept = True async def _worker(self): - async with self.scan._acatch(context=self._worker, unhandled_is_critical=True): - try: + try: + async with self.scan._acatch(context=self._worker, unhandled_is_critical=True): while not self.scan.stopping and not self.errored: try: if self.incoming_event_queue is not False: @@ -1863,16 +1894,19 @@ async def _worker(self): self.debug(f"Forwarding {event}") await self.forward_event(event, kwargs) - except asyncio.CancelledError: - # this trace was used for debugging leaked CancelledErrors from inside httpx - # self.log.trace("Worker cancelled") - raise - except BaseException as e: - if self.helpers.in_exception_chain(e, (KeyboardInterrupt,)): - self.scan.stop() - else: - self.critical(f"Critical failure in intercept module {self.name}: {e}") - self.critical(traceback.format_exc()) + except asyncio.CancelledError: + # this trace was used for debugging leaked CancelledErrors from inside httpx + # self.log.trace("Worker cancelled") + raise + except RuntimeError as e: + self.trace(f"RuntimeError in intercept module {self.name}: {e}") + except BaseException as e: + if self.helpers.in_exception_chain(e, (KeyboardInterrupt,)): + await self.scan.async_stop() + else: + self.critical(f"Critical failure in intercept module {self.name}: {e}") + self.critical(traceback.format_exc()) + self.log.trace("Worker stopped") async def get_incoming_event(self): diff --git a/bbot/modules/bevigil.py b/bbot/modules/bevigil.py index 8e70fe4143..82d9e00c90 100644 --- a/bbot/modules/bevigil.py +++ b/bbot/modules/bevigil.py @@ -8,7 +8,7 @@ class bevigil(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME", "URL_UNVERIFIED"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Retrieve OSINT data from mobile applications using BeVigil", "created_date": "2022-10-26", @@ -38,7 +38,7 @@ async def handle_event(self, event): subdomain, "DNS_NAME", parent=event, - context=f'{{module}} queried BeVigil\'s API for "{query}" and discovered {{event.type}}: {{event.data}}', + context=f'{{module}} queried BeVigil\'s API for "{query}" and discovered {{event.type}}: {{event.pretty_string}}', ) if self.urls: @@ -49,7 +49,7 @@ async def handle_event(self, event): parsed_url.geturl(), "URL_UNVERIFIED", parent=event, - context=f'{{module}} queried BeVigil\'s API for "{query}" and discovered {{event.type}}: {{event.data}}', + context=f'{{module}} queried BeVigil\'s API for "{query}" and discovered {{event.type}}: {{event.pretty_string}}', ) async def request_subdomains(self, query): diff --git a/bbot/modules/bucket_amazon.py b/bbot/modules/bucket_amazon.py index 9b0315c0ab..1b9d7fd788 100644 --- a/bbot/modules/bucket_amazon.py +++ b/bbot/modules/bucket_amazon.py @@ -4,7 +4,7 @@ class bucket_amazon(bucket_template): watched_events = ["DNS_NAME", "STORAGE_BUCKET"] produced_events = ["STORAGE_BUCKET", "FINDING"] - flags = ["active", "safe", "cloud-enum", "web-basic"] + flags = ["safe", "active", "cloud-enum", "web"] meta = { "description": "Check for S3 buckets related to target", "created_date": "2022-11-04", diff --git a/bbot/modules/bucket_digitalocean.py b/bbot/modules/bucket_digitalocean.py index 16701a0f1c..cd0e90a90c 100644 --- a/bbot/modules/bucket_digitalocean.py +++ b/bbot/modules/bucket_digitalocean.py @@ -4,7 +4,7 @@ class bucket_digitalocean(bucket_template): watched_events = ["DNS_NAME", "STORAGE_BUCKET"] produced_events = ["STORAGE_BUCKET", "FINDING"] - flags = ["active", "safe", "slow", "cloud-enum", "web-thorough"] + flags = ["safe", "active", "slow", "cloud-enum", "web-heavy"] meta = { "description": "Check for DigitalOcean spaces related to target", "created_date": "2022-11-08", diff --git a/bbot/modules/bucket_file_enum.py b/bbot/modules/bucket_file_enum.py index 15a429de93..8849970e4b 100644 --- a/bbot/modules/bucket_file_enum.py +++ b/bbot/modules/bucket_file_enum.py @@ -16,7 +16,7 @@ class bucket_file_enum(BaseModule): "created_date": "2023-11-14", "author": "@TheTechromancer", } - flags = ["passive", "safe", "cloud-enum"] + flags = ["safe", "passive", "cloud-enum"] options = { "file_limit": 50, } @@ -28,12 +28,11 @@ async def setup(self): return True async def handle_event(self, event): - cloud_tags = (t for t in event.tags if t.startswith("cloud-")) - if any(t.endswith("-amazon") or t.endswith("-digitalocean") for t in cloud_tags): + if "amazon" in event.tags or "digitalocean" in event.tags: await self.handle_aws(event) async def handle_aws(self, event): - url = event.data["url"] + url = event.url urls_emitted = 0 response = await self.helpers.request(url) status_code = getattr(response, "status_code", 0) @@ -52,7 +51,7 @@ async def handle_aws(self, event): "URL_UNVERIFIED", parent=event, tags="filedownload", - context=f"{{module}} enumerate files in bucket and discovered {extension_upper} file at {{event.type}}: {{event.data}}", + context=f"{{module}} enumerate files in bucket and discovered {extension_upper} file at {{event.type}}: {{event.pretty_string}}", ) urls_emitted += 1 if urls_emitted >= self.file_limit: diff --git a/bbot/modules/bucket_firebase.py b/bbot/modules/bucket_firebase.py index d78097829c..4859911a68 100644 --- a/bbot/modules/bucket_firebase.py +++ b/bbot/modules/bucket_firebase.py @@ -4,7 +4,7 @@ class bucket_firebase(bucket_template): watched_events = ["DNS_NAME", "STORAGE_BUCKET"] produced_events = ["STORAGE_BUCKET", "FINDING"] - flags = ["active", "safe", "cloud-enum", "web-basic"] + flags = ["safe", "active", "cloud-enum", "web"] meta = { "description": "Check for open Firebase databases related to target", "created_date": "2023-03-20", diff --git a/bbot/modules/bucket_google.py b/bbot/modules/bucket_google.py index 579453276d..338db450c4 100644 --- a/bbot/modules/bucket_google.py +++ b/bbot/modules/bucket_google.py @@ -8,7 +8,7 @@ class bucket_google(bucket_template): watched_events = ["DNS_NAME", "STORAGE_BUCKET"] produced_events = ["STORAGE_BUCKET", "FINDING"] - flags = ["active", "safe", "cloud-enum", "web-basic"] + flags = ["safe", "active", "cloud-enum", "web"] meta = { "description": "Check for Google object storage related to target", "created_date": "2022-11-04", diff --git a/bbot/modules/bucket_microsoft.py b/bbot/modules/bucket_microsoft.py index 17d2386f8e..9dc5924c0a 100644 --- a/bbot/modules/bucket_microsoft.py +++ b/bbot/modules/bucket_microsoft.py @@ -4,7 +4,7 @@ class bucket_microsoft(bucket_template): watched_events = ["DNS_NAME", "STORAGE_BUCKET"] produced_events = ["STORAGE_BUCKET", "FINDING"] - flags = ["active", "safe", "cloud-enum", "web-basic"] + flags = ["safe", "active", "cloud-enum", "web"] meta = { "description": "Check for Azure storage blobs related to target", "created_date": "2022-11-04", diff --git a/bbot/modules/bufferoverrun.py b/bbot/modules/bufferoverrun.py index 9523dc6269..18b10b2e56 100644 --- a/bbot/modules/bufferoverrun.py +++ b/bbot/modules/bufferoverrun.py @@ -4,7 +4,7 @@ class BufferOverrun(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query BufferOverrun's TLS API for subdomains", "created_date": "2024-10-23", diff --git a/bbot/modules/builtwith.py b/bbot/modules/builtwith.py index 6d0269cae6..054818c942 100644 --- a/bbot/modules/builtwith.py +++ b/bbot/modules/builtwith.py @@ -16,7 +16,7 @@ class builtwith(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["affiliates", "subdomain-enum", "passive", "safe"] + flags = ["safe", "affiliates", "subdomain-enum", "passive"] meta = { "description": "Query Builtwith.com for subdomains", "created_date": "2022-08-23", @@ -39,7 +39,7 @@ async def handle_event(self, event): s, "DNS_NAME", parent=event, - context=f'{{module}} queried the BuiltWith API for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} queried the BuiltWith API for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) # redirects if self.config.get("redirects", True): @@ -53,7 +53,7 @@ async def handle_event(self, event): "DNS_NAME", parent=event, tags=["affiliate"], - context=f'{{module}} queried the BuiltWith redirect API for "{query}" and found redirect to {{event.type}}: {{event.data}}', + context=f'{{module}} queried the BuiltWith redirect API for "{query}" and found redirect to {{event.type}}: {{event.pretty_string}}', ) async def request_domains(self, query): diff --git a/bbot/modules/bypass403.py b/bbot/modules/bypass403.py index 61fb510775..65cae080b4 100644 --- a/bbot/modules/bypass403.py +++ b/bbot/modules/bypass403.py @@ -63,8 +63,6 @@ "X-Host": "127.0.0.1", } -# This is planned to be replaced in the future: https://github.com/blacklanternsecurity/bbot/issues/1068 -waf_strings = ["The requested URL was rejected"] for qp in query_payloads: signatures.append(("GET", "{scheme}://{netloc}/{path}%s" % qp, None, True)) @@ -78,7 +76,7 @@ class bypass403(BaseModule): watched_events = ["URL"] produced_events = ["FINDING"] - flags = ["active", "aggressive", "web-thorough"] + flags = ["active", "loud", "web-heavy"] meta = {"description": "Check 403 pages for common bypasses", "created_date": "2022-07-05", "author": "@liquidsec"} in_scope_only = True @@ -88,7 +86,7 @@ async def do_checks(self, compare_helper, event, collapse_threshold): for sig in signatures: if error_count > 3: - self.warning(f"Received too many errors for URL {event.data} aborting bypass403") + self.warning(f"Received too many errors for URL {event.url} aborting bypass403") return None sig = self.format_signature(sig, event) @@ -107,8 +105,8 @@ async def do_checks(self, compare_helper, event, collapse_threshold): # In some cases WAFs will respond with a 200 code which causes a false positive if subject_response is not None: - for ws in waf_strings: - if ws in subject_response.text: + for waf_string in self.helpers.get_waf_strings(): + if waf_string in subject_response.text: self.debug("Rejecting result based on presence of WAF string") return @@ -129,7 +127,7 @@ async def do_checks(self, compare_helper, event, collapse_threshold): async def handle_event(self, event): try: - compare_helper = self.helpers.http_compare(event.data, allow_redirects=True) + compare_helper = self.helpers.http_compare(event.url, allow_redirects=True) except HttpCompareError as e: self.debug(e) return @@ -141,21 +139,31 @@ async def handle_event(self, event): if len(results) > collapse_threshold: await self.emit_event( { + "name": "Possible 403 Bypass", "description": f"403 Bypass MULTIPLE SIGNATURES (exceeded threshold {str(collapse_threshold)})", "host": str(event.host), - "url": event.data, + "url": event.url, + "severity": "INFO", + "confidence": "LOW", }, "FINDING", parent=event, - context=f"{{module}} discovered multiple potential 403 bypasses ({{event.type}}) for {event.data}", + context=f"{{module}} discovered multiple potential 403 bypasses ({{event.type}}) for {event.url}", ) else: for description in results: await self.emit_event( - {"description": description, "host": str(event.host), "url": event.data}, + { + "name": "Possible 403 Bypass", + "description": description, + "host": str(event.host), + "url": event.url, + "severity": "MEDIUM", + "confidence": "LOW", + }, "FINDING", parent=event, - context=f"{{module}} discovered potential 403 bypass ({{event.type}}) for {event.data}", + context=f"{{module}} discovered potential 403 bypass ({{event.type}}) for {event.url}", ) # When a WAF-check helper is available in the future, we will convert to HTTP_RESPONSE and check for the WAF string here. diff --git a/bbot/modules/c99.py b/bbot/modules/c99.py index 5f3792b45f..84b3eafc6c 100644 --- a/bbot/modules/c99.py +++ b/bbot/modules/c99.py @@ -4,7 +4,7 @@ class c99(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query the C99 API for subdomains", "created_date": "2022-07-08", diff --git a/bbot/modules/censys_dns.py b/bbot/modules/censys_dns.py index f8b9c62753..7e4a397eb8 100644 --- a/bbot/modules/censys_dns.py +++ b/bbot/modules/censys_dns.py @@ -9,7 +9,7 @@ class censys_dns(censys): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query the Censys API for subdomains", "created_date": "2022-08-04", diff --git a/bbot/modules/censys_ip.py b/bbot/modules/censys_ip.py index fa82dc33c7..374323b8fe 100644 --- a/bbot/modules/censys_ip.py +++ b/bbot/modules/censys_ip.py @@ -16,7 +16,7 @@ class censys_ip(censys): "TECHNOLOGY", "PROTOCOL", ] - flags = ["passive", "safe"] + flags = ["safe", "passive"] meta = { "description": "Query the Censys API for hosts by IP address", "created_date": "2026-01-26", @@ -33,9 +33,10 @@ class censys_ip(censys): async def setup(self): self.dns_names_limit = self.config.get("dns_names_limit", 100) - self.warning( - "This module may consume a lot of API queries. Unless you specifically want to query on each individual IP, we recommend using the censys_dns module instead." - ) + if not self.config.get("in_scope_only", True): + self.warning( + "in_scope_only is disabled. This module queries each IP individually and may consume a lot of API credits!" + ) return await super().setup() async def filter_event(self, event): @@ -126,7 +127,7 @@ async def handle_event(self, event): uri, "URL_UNVERIFIED", parent=event, - context="{module} found {event.data} in HTTP service of {event.parent.data}", + context="{module} found {event.pretty_string} in HTTP service of {event.parent.data}", ) # Extract TLS certificate data @@ -165,13 +166,13 @@ async def _emit_host(self, host, event, seen, source): # Validate and emit as DNS_NAME try: validated = self.helpers.validators.validate_host(host) + if validated and validated not in seen: + seen.add(validated) + await self.emit_event( + validated, + "DNS_NAME", + parent=event, + context=f"{{module}} found {{event.pretty_string}} in {source} of {{event.parent.data}}", + ) except ValueError as e: self.debug(f"Error validating host {host} in {source}: {e}") - if validated and validated not in seen: - seen.add(validated) - await self.emit_event( - validated, - "DNS_NAME", - parent=event, - context=f"{{module}} found {{event.data}} in {source} of {{event.parent.data}}", - ) diff --git a/bbot/modules/certspotter.py b/bbot/modules/certspotter.py index f8aa0fad41..69b108080c 100644 --- a/bbot/modules/certspotter.py +++ b/bbot/modules/certspotter.py @@ -4,7 +4,7 @@ class certspotter(subdomain_enum): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query Certspotter's API for subdomains", "created_date": "2022-07-28", diff --git a/bbot/modules/chaos.py b/bbot/modules/chaos.py index 15a321046a..9c4255e542 100644 --- a/bbot/modules/chaos.py +++ b/bbot/modules/chaos.py @@ -4,7 +4,7 @@ class chaos(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query ProjectDiscovery's Chaos API for subdomains", "created_date": "2022-08-14", diff --git a/bbot/modules/code_repository.py b/bbot/modules/code_repository.py index f485579f9e..32260c5970 100644 --- a/bbot/modules/code_repository.py +++ b/bbot/modules/code_repository.py @@ -10,7 +10,7 @@ class code_repository(BaseModule): "created_date": "2024-05-15", "author": "@domwhewell-sage", } - flags = ["passive", "safe", "code-enum"] + flags = ["safe", "passive", "code-enum"] # platform name : (regex, case_sensitive) code_repositories = { @@ -39,7 +39,7 @@ async def handle_event(self, event): if not isinstance(regexes, list): regexes = [regexes] for regex, case_sensitive in regexes: - for match in regex.finditer(event.data): + for match in regex.finditer(event.url): url = match.group() if not case_sensitive: url = url.lower() diff --git a/bbot/modules/credshed.py b/bbot/modules/credshed.py index 3630646a6f..ea1bf707c1 100644 --- a/bbot/modules/credshed.py +++ b/bbot/modules/credshed.py @@ -6,7 +6,7 @@ class credshed(subdomain_enum): watched_events = ["DNS_NAME"] produced_events = ["PASSWORD", "HASHED_PASSWORD", "USERNAME", "EMAIL_ADDRESS"] - flags = ["passive", "safe"] + flags = ["safe", "passive"] meta = { "description": "Send queries to your own credshed server to check for known credentials of your targets", "created_date": "2023-10-12", @@ -80,7 +80,8 @@ async def handle_event(self, event): email_event = self.make_event(email, "EMAIL_ADDRESS", parent=event, tags=tags) if email_event is not None: await self.emit_event( - email_event, context=f'{{module}} searched for "{query}" and found {{event.type}}: {{event.data}}' + email_event, + context=f'{{module}} searched for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) if user: await self.emit_event( @@ -88,7 +89,7 @@ async def handle_event(self, event): "USERNAME", parent=email_event, tags=tags, - context=f"{{module}} found {email} with {{event.type}}: {{event.data}}", + context=f"{{module}} found {email} with {{event.type}}: {{event.pretty_string}}", ) if pw: await self.emit_event( @@ -96,7 +97,7 @@ async def handle_event(self, event): "PASSWORD", parent=email_event, tags=tags, - context=f"{{module}} found {email} with {{event.type}}: {{event.data}}", + context=f"{{module}} found {email} with {{event.type}}: {{event.pretty_string}}", ) for h_pw in hashes: if h_pw: @@ -105,5 +106,5 @@ async def handle_event(self, event): "HASHED_PASSWORD", parent=email_event, tags=tags, - context=f"{{module}} found {email} with {{event.type}}: {{event.data}}", + context=f"{{module}} found {email} with {{event.type}}: {{event.pretty_string}}", ) diff --git a/bbot/modules/crt.py b/bbot/modules/crt.py index 05735c4e93..60ea6b6a0c 100644 --- a/bbot/modules/crt.py +++ b/bbot/modules/crt.py @@ -2,7 +2,7 @@ class crt(subdomain_enum): - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { diff --git a/bbot/modules/crt_db.py b/bbot/modules/crt_db.py index 71c52a4bf8..4008394f1d 100644 --- a/bbot/modules/crt_db.py +++ b/bbot/modules/crt_db.py @@ -5,7 +5,7 @@ class crt_db(subdomain_enum): - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { diff --git a/bbot/modules/dehashed.py b/bbot/modules/dehashed.py index 4e64f22450..701425407f 100644 --- a/bbot/modules/dehashed.py +++ b/bbot/modules/dehashed.py @@ -6,7 +6,7 @@ class dehashed(subdomain_enum): watched_events = ["DNS_NAME"] produced_events = ["PASSWORD", "HASHED_PASSWORD", "USERNAME", "EMAIL_ADDRESS"] - flags = ["passive", "safe", "email-enum"] + flags = ["safe", "passive", "email-enum"] meta = { "description": "Execute queries against dehashed.com for exposed credentials", "created_date": "2023-10-12", @@ -60,7 +60,7 @@ async def handle_event(self, event): if email_event is not None: await self.emit_event( email_event, - context=f'{{module}} searched API for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched API for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) for user in users: await self.emit_event( @@ -68,7 +68,7 @@ async def handle_event(self, event): "USERNAME", parent=email_event, tags=tags, - context=f"{{module}} found {email} with {{event.type}}: {{event.data}}", + context=f"{{module}} found {email} with {{event.type}}: {{event.pretty_string}}", ) for pw in pws: await self.emit_event( @@ -76,7 +76,7 @@ async def handle_event(self, event): "PASSWORD", parent=email_event, tags=tags, - context=f"{{module}} found {email} with {{event.type}}: {{event.data}}", + context=f"{{module}} found {email} with {{event.type}}: {{event.pretty_string}}", ) for h_pw in h_pws: await self.emit_event( @@ -84,7 +84,7 @@ async def handle_event(self, event): "HASHED_PASSWORD", parent=email_event, tags=tags, - context=f"{{module}} found {email} with {{event.type}}: {{event.data}}", + context=f"{{module}} found {email} with {{event.type}}: {{event.pretty_string}}", ) async def query(self, domain): diff --git a/bbot/modules/digitorus.py b/bbot/modules/digitorus.py index 049343ac27..611d53704a 100644 --- a/bbot/modules/digitorus.py +++ b/bbot/modules/digitorus.py @@ -4,7 +4,7 @@ class digitorus(subdomain_enum): - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { diff --git a/bbot/modules/dnsbimi.py b/bbot/modules/dnsbimi.py index d108933ea4..4148dae509 100644 --- a/bbot/modules/dnsbimi.py +++ b/bbot/modules/dnsbimi.py @@ -46,7 +46,7 @@ class dnsbimi(BaseModule): watched_events = ["DNS_NAME"] produced_events = ["URL_UNVERIFIED", "RAW_DNS_RECORD"] - flags = ["subdomain-enum", "cloud-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "cloud-enum", "passive"] meta = { "description": "Check DNS_NAME's for BIMI records to find image and certificate hosting URL's", "author": "@colin-stubbs", diff --git a/bbot/modules/dnsbrute.py b/bbot/modules/dnsbrute.py index 4c57d1feda..77df6124ac 100644 --- a/bbot/modules/dnsbrute.py +++ b/bbot/modules/dnsbrute.py @@ -2,7 +2,7 @@ class dnsbrute(subdomain_enum): - flags = ["subdomain-enum", "active", "aggressive"] + flags = ["subdomain-enum", "active", "loud"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { @@ -54,11 +54,11 @@ async def filter_event(self, event): async def handle_event(self, event): query = self.make_query(event) - self.info(f"Brute-forcing {self.wordlist_size:,} subdomains for {query} (source: {event.data})") + self.info(f"Brute-forcing {self.wordlist_size:,} subdomains for {query} (source: {event.pretty_string})") for hostname in await self.helpers.dns.brute(self, query, self.subdomain_list): await self.emit_event( hostname, "DNS_NAME", parent=event, - context=f'{{module}} tried {self.wordlist_size:,} subdomains against "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} tried {self.wordlist_size:,} subdomains against "{query}" and found {{event.type}}: {{event.pretty_string}}', ) diff --git a/bbot/modules/dnsbrute_mutations.py b/bbot/modules/dnsbrute_mutations.py index f56a214572..aeb695fb6d 100644 --- a/bbot/modules/dnsbrute_mutations.py +++ b/bbot/modules/dnsbrute_mutations.py @@ -4,7 +4,7 @@ class dnsbrute_mutations(BaseModule): - flags = ["subdomain-enum", "active", "aggressive", "slow"] + flags = ["subdomain-enum", "active", "loud", "slow"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { @@ -139,7 +139,7 @@ def add_mutation(m): parent=parent_event, tags=[f"mutation-{mutation_run}"], abort_if=self.abort_if, - context=f'{{module}} found a mutated subdomain of "{parent_event.host}" on its {mutation_run_ordinal} run: {{event.type}}: {{event.data}}', + context=f'{{module}} found a mutated subdomain of "{parent_event.host}" on its {mutation_run_ordinal} run: {{event.type}}: {{event.pretty_string}}', ) if results: continue diff --git a/bbot/modules/dnscaa.py b/bbot/modules/dnscaa.py index 1465cd8faf..fad74bd947 100644 --- a/bbot/modules/dnscaa.py +++ b/bbot/modules/dnscaa.py @@ -41,7 +41,7 @@ class dnscaa(BaseModule): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME", "EMAIL_ADDRESS", "URL_UNVERIFIED"] - flags = ["subdomain-enum", "email-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "email-enum", "passive"] meta = {"description": "Check for CAA records", "author": "@colin-stubbs", "created_date": "2024-05-26"} options = { "in_scope_only": True, diff --git a/bbot/modules/dnscommonsrv.py b/bbot/modules/dnscommonsrv.py index d12932079b..65bff03a2f 100644 --- a/bbot/modules/dnscommonsrv.py +++ b/bbot/modules/dnscommonsrv.py @@ -5,7 +5,7 @@ class dnscommonsrv(subdomain_enum): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "active", "safe"] + flags = ["safe", "subdomain-enum", "active"] meta = {"description": "Check for common SRV records", "created_date": "2022-05-15", "author": "@TheTechromancer"} dedup_strategy = "lowest_parent" deps_common = ["massdns"] @@ -32,5 +32,5 @@ async def handle_event(self, event): hostname, "DNS_NAME", parent=event, - context=f'{{module}} tried {self.num_srvs:,} common SRV records against "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} tried {self.num_srvs:,} common SRV records against "{query}" and found {{event.type}}: {{event.pretty_string}}', ) diff --git a/bbot/modules/dnsdumpster.py b/bbot/modules/dnsdumpster.py index 2a5b7539b0..63fd34b596 100644 --- a/bbot/modules/dnsdumpster.py +++ b/bbot/modules/dnsdumpster.py @@ -6,7 +6,7 @@ class dnsdumpster(subdomain_enum): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query dnsdumpster for subdomains", "created_date": "2022-03-12", diff --git a/bbot/modules/dnstlsrpt.py b/bbot/modules/dnstlsrpt.py index 2ccd6f676c..8d2976e93c 100644 --- a/bbot/modules/dnstlsrpt.py +++ b/bbot/modules/dnstlsrpt.py @@ -34,7 +34,7 @@ class dnstlsrpt(BaseModule): watched_events = ["DNS_NAME"] produced_events = ["EMAIL_ADDRESS", "URL_UNVERIFIED", "RAW_DNS_RECORD"] - flags = ["subdomain-enum", "cloud-enum", "email-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "cloud-enum", "email-enum", "passive"] meta = { "description": "Check for TLS-RPT records", "author": "@colin-stubbs", diff --git a/bbot/modules/docker_pull.py b/bbot/modules/docker_pull.py index b51de151ce..ad702cc5e3 100644 --- a/bbot/modules/docker_pull.py +++ b/bbot/modules/docker_pull.py @@ -8,7 +8,7 @@ class docker_pull(BaseModule): watched_events = ["CODE_REPOSITORY"] produced_events = ["FILESYSTEM"] - flags = ["passive", "safe", "slow", "code-enum", "download"] + flags = ["safe", "passive", "slow", "code-enum", "download"] meta = { "description": "Download images from a docker repository", "created_date": "2024-03-24", @@ -49,7 +49,7 @@ async def filter_event(self, event): return True async def handle_event(self, event): - repo_url = event.data.get("url") + repo_url = event.url repo_path = await self.download_docker_repo(repo_url) if repo_path: self.verbose(f"Downloaded docker repository {repo_url} to {repo_path}") diff --git a/bbot/modules/dockerhub.py b/bbot/modules/dockerhub.py index 91cdc64ea5..c8fd2ce5cd 100644 --- a/bbot/modules/dockerhub.py +++ b/bbot/modules/dockerhub.py @@ -4,7 +4,7 @@ class dockerhub(BaseModule): watched_events = ["SOCIAL", "ORG_STUB"] produced_events = ["SOCIAL", "CODE_REPOSITORY", "URL_UNVERIFIED"] - flags = ["passive", "safe", "code-enum"] + flags = ["safe", "passive", "code-enum"] meta = { "description": "Search for docker repositories of discovered orgs/usernames", "created_date": "2024-03-12", @@ -43,7 +43,7 @@ async def handle_org_stub(self, event): {"platform": "docker", "url": site_url, "profile_name": p}, "SOCIAL", parent=event, - context=f"{{module}} tried {event.type} {event.data} and found docker profile ({{event.type}}) at {p}", + context=f"{{module}} tried {event.type} {event.pretty_string} and found docker profile ({{event.type}}) at {p}", ) async def handle_social(self, event): diff --git a/bbot/modules/dotnetnuke.py b/bbot/modules/dotnetnuke.py index 7e8b4d3d4e..539eab69a5 100644 --- a/bbot/modules/dotnetnuke.py +++ b/bbot/modules/dotnetnuke.py @@ -18,8 +18,8 @@ class dotnetnuke(BaseModule): } watched_events = ["HTTP_RESPONSE"] - produced_events = ["VULNERABILITY", "TECHNOLOGY"] - flags = ["active", "aggressive", "web-thorough"] + produced_events = ["FINDING", "TECHNOLOGY"] + flags = ["active", "loud", "invasive", "web-heavy"] meta = { "description": "Scan for critical DotNetNuke (DNN) vulnerabilities", "created_date": "2023-11-21", @@ -47,16 +47,19 @@ async def interactsh_callback(self, r): event = self.interactsh_subdomain_tags.get(full_id.split(".")[0]) if not event: return - url = event.data["url"] + url = event.url description = "DotNetNuke Blind-SSRF (CVE 2017-0929)" await self.emit_event( { "severity": "MEDIUM", + "confidence": "HIGH", "host": str(event.host), "url": url, "description": description, + "cves": ["CVE-2017-0929"], + "name": "DotNetNuke Blind-SSRF", }, - "VULNERABILITY", + "FINDING", event, context=f"{{module}} scanned {url} and found medium {{event.type}}: {description}", ) @@ -71,7 +74,7 @@ async def handle_event(self, event): if raw_headers: for header_signature in self.DNN_signatures_header: if header_signature in raw_headers: - url = event.data["url"] + url = event.url await self.emit_event( {"technology": "DotNetNuke", "url": url, "host": str(event.host)}, "TECHNOLOGY", @@ -85,17 +88,17 @@ async def handle_event(self, event): for body_signature in self.DNN_signatures_body: if body_signature in resp_body: await self.emit_event( - {"technology": "DotNetNuke", "url": event.data["url"], "host": str(event.host)}, + {"technology": "DotNetNuke", "url": event.url, "host": str(event.host)}, "TECHNOLOGY", event, - context=f"{{module}} scanned {event.data['url']} and found {{event.type}}: DotNetNuke", + context=f"{{module}} scanned {event.url} and found {{event.type}}: DotNetNuke", ) detected = True break if detected is True: # DNNPersonalization Deserialization Detection - for probe_url in [f"{event.data['url']}/__", f"{event.data['url']}/", f"{event.data['url']}"]: + for probe_url in [f"{event.url}/__", f"{event.url}/", f"{event.url}"]: result = await self.helpers.request(probe_url, cookies=self.exploit_probe) if result: if "for 16-bit app support" in result.text and "[extensions]" in result.text: @@ -103,11 +106,13 @@ async def handle_event(self, event): await self.emit_event( { "severity": "CRITICAL", + "confidence": "CONFIRMED", "description": description, "host": str(event.host), "url": probe_url, + "name": "DotNetNuke Cookie Deserialization", }, - "VULNERABILITY", + "FINDING", event, context=f"{{module}} scanned {probe_url} and found critical {{event.type}}: {description}", ) @@ -115,7 +120,7 @@ async def handle_event(self, event): if "endpoint" not in event.tags: # NewsArticlesSlider ImageHandler.ashx File Read result = await self.helpers.request( - f"{event.data['url']}/DesktopModules/dnnUI_NewsArticlesSlider/ImageHandler.ashx?img=~/web.config" + f"{event.url}/DesktopModules/dnnUI_NewsArticlesSlider/ImageHandler.ashx?img=~/web.config" ) if result: if "" in result.text: @@ -123,18 +128,20 @@ async def handle_event(self, event): await self.emit_event( { "severity": "CRITICAL", + "confidence": "CONFIRMED", "description": description, + "name": "DotNetNuke Arbitrary File Read", "host": str(event.host), - "url": f"{event.data['url']}/DesktopModules/dnnUI_NewsArticlesSlider/ImageHandler.ashx", + "url": f"{event.url}/DesktopModules/dnnUI_NewsArticlesSlider/ImageHandler.ashx", }, - "VULNERABILITY", + "FINDING", event, - context=f"{{module}} scanned {event.data['url']} and found critical {{event.type}}: {description}", + context=f"{{module}} scanned {event.url} and found critical {{event.type}}: {description}", ) # DNNArticle GetCSS.ashx File Read result = await self.helpers.request( - f"{event.data['url']}/DesktopModules/DNNArticle/getcss.ashx?CP=%2fweb.config&smid=512&portalid=3" + f"{event.url}/DesktopModules/DNNArticle/getcss.ashx?CP=%2fweb.config&smid=512&portalid=3" ) if result: if "" in result.text: @@ -142,45 +149,49 @@ async def handle_event(self, event): await self.emit_event( { "severity": "CRITICAL", + "confidence": "CONFIRMED", "description": description, + "name": "DotNetNuke Arbitrary File Read", "host": str(event.host), - "url": f"{event.data['url']}/Desktopmodules/DNNArticle/GetCSS.ashx/?CP=%2fweb.config", + "url": f"{event.url}/Desktopmodules/DNNArticle/GetCSS.ashx/?CP=%2fweb.config", }, - "VULNERABILITY", + "FINDING", event, - context=f"{{module}} scanned {event.data['url']} and found critical {{event.type}}: {description}", + context=f"{{module}} scanned {event.url} and found critical {{event.type}}: {description}", ) # InstallWizard SuperUser Privilege Escalation - result = await self.helpers.request(f"{event.data['url']}/Install/InstallWizard.aspx") + result = await self.helpers.request(f"{event.url}/Install/InstallWizard.aspx") if result: if result.status_code == 200: result_confirm = await self.helpers.request( - f"{event.data['url']}/Install/InstallWizard.aspx?__viewstate=1" + f"{event.url}/Install/InstallWizard.aspx?__viewstate=1" ) if result_confirm.status_code == 500: description = "DotNetNuke InstallWizard SuperUser Privilege Escalation" await self.emit_event( { "severity": "CRITICAL", + "confidence": "CONFIRMED", "description": description, + "name": "DotNetNuke Privilege Escalation", "host": str(event.host), - "url": f"{event.data['url']}/Install/InstallWizard.aspx", + "url": f"{event.url}/Install/InstallWizard.aspx", }, - "VULNERABILITY", + "FINDING", event, - context=f"{{module}} scanned {event.data['url']} and found critical {{event.type}}: {description}", + context=f"{{module}} scanned {event.url} and found critical {{event.type}}: {description}", ) return # DNNImageHandler.ashx Blind SSRF - self.event_dict[event.data["url"]] = event + self.event_dict[event.url] = event if self.interactsh_instance: subdomain_tag = self.helpers.rand_string(4, digits=False) self.interactsh_subdomain_tags[subdomain_tag] = event await self.helpers.request( - f"{event.data['url']}/DnnImageHandler.ashx?mode=file&url=http://{subdomain_tag}.{self.interactsh_domain}" + f"{event.url}/DnnImageHandler.ashx?mode=file&url=http://{subdomain_tag}.{self.interactsh_domain}" ) else: self.debug( diff --git a/bbot/modules/emailformat.py b/bbot/modules/emailformat.py index e1d7d74aff..560d03c8ed 100644 --- a/bbot/modules/emailformat.py +++ b/bbot/modules/emailformat.py @@ -4,7 +4,7 @@ class emailformat(BaseModule): watched_events = ["DNS_NAME"] produced_events = ["EMAIL_ADDRESS"] - flags = ["passive", "email-enum", "safe"] + flags = ["safe", "passive", "email-enum"] meta = { "description": "Query email-format.com for email addresses", "created_date": "2022-07-11", @@ -43,5 +43,5 @@ async def handle_event(self, event): email, "EMAIL_ADDRESS", parent=event, - context=f'{{module}} searched email-format.com for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched email-format.com for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) diff --git a/bbot/modules/ffuf.py b/bbot/modules/ffuf.py index 038aa5d268..4076b02024 100644 --- a/bbot/modules/ffuf.py +++ b/bbot/modules/ffuf.py @@ -9,7 +9,7 @@ class ffuf(BaseModule): watched_events = ["URL"] produced_events = ["URL_UNVERIFIED"] - flags = ["aggressive", "active", "deadly"] + flags = ["loud", "active"] meta = {"description": "A fast web fuzzer written in Go", "created_date": "2022-04-10", "author": "@liquidsec"} options = { @@ -59,7 +59,7 @@ async def setup(self): return True async def handle_event(self, event): - if self.helpers.url_depth(event.data) > self.config.get("max_depth"): + if self.helpers.url_depth(event.url) > self.config.get("max_depth"): self.debug("Exceeded max depth, aborting event") return @@ -69,7 +69,7 @@ async def handle_event(self, event): return else: # if we think its a directory, normalize it. - fixed_url = event.data.rstrip("/") + "/" + fixed_url = event.url.rstrip("/") + "/" exts = ["", "/"] if self.extensions: @@ -83,12 +83,12 @@ async def handle_event(self, event): "URL_UNVERIFIED", parent=event, tags=[f"status-{r['status']}"], - context=f"{{module}} brute-forced {event.data} and found {{event.type}}: {{event.data}}", + context=f"{{module}} brute-forced {event.url} and found {{event.type}}: {{event.pretty_string}}", ) async def filter_event(self, event): if "endpoint" in event.tags: - self.debug(f"rejecting URL [{event.data}] because we don't ffuf endpoints") + self.debug(f"rejecting URL [{event.url}] because we don't ffuf endpoints") return False return True diff --git a/bbot/modules/ffuf_shortnames.py b/bbot/modules/ffuf_shortnames.py index ee6e040095..f002b4e613 100644 --- a/bbot/modules/ffuf_shortnames.py +++ b/bbot/modules/ffuf_shortnames.py @@ -9,7 +9,7 @@ class ffuf_shortnames(ffuf): watched_events = ["URL_HINT"] produced_events = ["URL_UNVERIFIED"] - flags = ["aggressive", "active", "iis-shortnames", "web-thorough"] + flags = ["loud", "active", "iis-shortnames", "web-heavy"] meta = { "description": "Use ffuf in combination IIS shortnames", "created_date": "2022-07-05", @@ -213,10 +213,10 @@ async def handle_event(self, event): host = f"{event.parent.parsed_url.scheme}://{event.parent.parsed_url.netloc}/" if host not in self.per_host_collection.keys(): - self.per_host_collection[host] = [(filename_hint, event.parent.data)] + self.per_host_collection[host] = [(filename_hint, event.parent.url)] else: - self.per_host_collection[host].append((filename_hint, event.parent.data)) + self.per_host_collection[host].append((filename_hint, event.parent.url)) self.shortname_to_event[filename_hint] = event @@ -245,7 +245,7 @@ async def handle_event(self, event): "URL_UNVERIFIED", parent=event, tags=[f"status-{r['status']}"], - context=f"{{module}} brute-forced {ext.upper()} files at {root_url} and found {{event.type}}: {{event.data}}", + context=f"{{module}} brute-forced {ext.upper()} files at {root_url} and found {{event.type}}: {{event.pretty_string}}", ) elif shortname_type == "directory": @@ -256,7 +256,7 @@ async def handle_event(self, event): "URL_UNVERIFIED", parent=event, tags=[f"status-{r['status']}"], - context=f"{{module}} brute-forced directories at {r_url} and found {{event.type}}: {{event.data}}", + context=f"{{module}} brute-forced directories at {r_url} and found {{event.type}}: {{event.pretty_string}}", ) if self.config.get("find_delimiters"): @@ -273,7 +273,7 @@ async def handle_event(self, event): "URL_UNVERIFIED", parent=event, tags=[f"status-{r['status']}"], - context=f'{{module}} brute-forced directories with detected prefix "{ffuf_prefix}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} brute-forced directories with detected prefix "{ffuf_prefix}" and found {{event.type}}: {{event.pretty_string}}', ) elif "shortname-endpoint" in event.tags: @@ -290,7 +290,7 @@ async def handle_event(self, event): "URL_UNVERIFIED", parent=event, tags=[f"status-{r['status']}"], - context=f'{{module}} brute-forced {ext.upper()} files with detected prefix "{ffuf_prefix}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} brute-forced {ext.upper()} files with detected prefix "{ffuf_prefix}" and found {{event.type}}: {{event.pretty_string}}', ) if self.config.get("find_subwords"): @@ -304,7 +304,7 @@ async def handle_event(self, event): "URL_UNVERIFIED", parent=event, tags=[f"status-{r['status']}"], - context=f'{{module}} brute-forced directories with detected subword "{subword}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} brute-forced directories with detected subword "{subword}" and found {{event.type}}: {{event.pretty_string}}', ) elif "shortname-endpoint" in event.tags: for ext in used_extensions: @@ -315,7 +315,7 @@ async def handle_event(self, event): "URL_UNVERIFIED", parent=event, tags=[f"status-{r['status']}"], - context=f'{{module}} brute-forced {ext.upper()} files with detected subword "{subword}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} brute-forced {ext.upper()} files with detected subword "{subword}" and found {{event.type}}: {{event.pretty_string}}', ) async def finish(self): @@ -357,7 +357,7 @@ async def finish(self): "URL_UNVERIFIED", parent=self.shortname_to_event[hint], tags=[f"status-{r['status']}"], - context=f'{{module}} brute-forced directories with common prefix "{prefix}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} brute-forced directories with common prefix "{prefix}" and found {{event.type}}: {{event.pretty_string}}', ) elif shortname_type == "endpoint": used_extensions = self.build_extension_list(self.shortname_to_event[hint]) @@ -374,5 +374,5 @@ async def finish(self): "URL_UNVERIFIED", parent=self.shortname_to_event[hint], tags=[f"status-{r['status']}"], - context=f'{{module}} brute-forced {ext.upper()} files with common prefix "{prefix}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} brute-forced {ext.upper()} files with common prefix "{prefix}" and found {{event.type}}: {{event.pretty_string}}', ) diff --git a/bbot/modules/filedownload.py b/bbot/modules/filedownload.py index 0f113f41ec..adb50f7857 100644 --- a/bbot/modules/filedownload.py +++ b/bbot/modules/filedownload.py @@ -14,7 +14,7 @@ class filedownload(BaseModule): watched_events = ["URL_UNVERIFIED", "HTTP_RESPONSE"] produced_events = ["FILESYSTEM"] - flags = ["active", "safe", "web-basic", "download"] + flags = ["safe", "active", "web", "download"] meta = { "description": "Download common filetypes such as PDF, DOCX, PPTX, etc.", "created_date": "2023-10-11", @@ -131,22 +131,20 @@ async def filter_event(self, event): return True def hash_event(self, event): - if event.type == "HTTP_RESPONSE": - return hash(event.data["url"]) - return hash(event.data) + return hash(event.url or event.data) async def handle_event(self, event): if event.type == "URL_UNVERIFIED": - url_lower = event.data.lower() + url_lower = event.url.lower() extension_matches = any(url_lower.endswith(f".{e}") for e in self.extensions) filedownload_requested = "filedownload" in event.tags if extension_matches or filedownload_requested: - await self.download_file(event.data, source_event=event) + await self.download_file(event.url, source_event=event) elif event.type == "HTTP_RESPONSE": headers = event.data.get("header", {}) content_type = headers.get("content_type", "") if content_type: - url = event.data["url"] + url = event.url await self.download_file(url, content_type=content_type, source_event=event) async def download_file(self, url, content_type=None, source_event=None): diff --git a/bbot/modules/fingerprintx.py b/bbot/modules/fingerprintx.py index cd72ae1d0e..bea096dfeb 100644 --- a/bbot/modules/fingerprintx.py +++ b/bbot/modules/fingerprintx.py @@ -6,7 +6,7 @@ class fingerprintx(BaseModule): watched_events = ["OPEN_TCP_PORT"] produced_events = ["PROTOCOL"] - flags = ["active", "safe", "service-enum", "slow"] + flags = ["safe", "active", "service-enum", "slow"] meta = { "description": "Fingerprint exposed services like RDP, SSH, MySQL, etc.", "created_date": "2023-01-30", @@ -80,8 +80,6 @@ async def handle_batch(self, *events): banner = j.get("metadata", {}).get("banner", "").strip() port_data = f"{host}:{port}" tags = set() - if host and ip: - tags.add(f"ip-{ip}") parent_event = _input.get(port_data) protocol_data = {"host": host, "protocol": protocol} if port: diff --git a/bbot/modules/fullhunt.py b/bbot/modules/fullhunt.py index 13a3de5f56..584dafb143 100644 --- a/bbot/modules/fullhunt.py +++ b/bbot/modules/fullhunt.py @@ -4,7 +4,7 @@ class fullhunt(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query the fullhunt.io API for subdomains", "created_date": "2022-08-24", diff --git a/bbot/modules/generic_ssrf.py b/bbot/modules/generic_ssrf.py deleted file mode 100644 index 6ccde510b9..0000000000 --- a/bbot/modules/generic_ssrf.py +++ /dev/null @@ -1,262 +0,0 @@ -from bbot.errors import InteractshError -from bbot.modules.base import BaseModule - - -ssrf_params = [ - "Dest", - "Redirect", - "URI", - "Path", - "Continue", - "URL", - "Window", - "Next", - "Data", - "Reference", - "Site", - "HTML", - "Val", - "Validate", - "Domain", - "Callback", - "Return", - "Page", - "Feed", - "Host", - "Port", - "To", - "Out", - "View", - "Dir", - "Show", - "Navigation", - "Open", -] - - -class BaseSubmodule: - technique_description = "base technique description" - severity = "INFO" - paths = [] - - def __init__(self, generic_ssrf): - self.generic_ssrf = generic_ssrf - self.test_paths = self.create_paths() - - def set_base_url(self, event): - return f"{event.parsed_url.scheme}://{event.parsed_url.netloc}" - - def create_paths(self): - return self.paths - - async def test(self, event): - base_url = self.set_base_url(event) - for test_path_result in self.test_paths: - for lower in [True, False]: - test_path = test_path_result[0] - if lower: - test_path = test_path.lower() - subdomain_tag = test_path_result[1] - test_url = f"{base_url}{test_path}" - self.generic_ssrf.debug(f"Sending request to URL: {test_url}") - r = await self.generic_ssrf.helpers.curl(url=test_url) - if r: - self.process(event, r, subdomain_tag) - - def process(self, event, r, subdomain_tag): - response_token = self.generic_ssrf.interactsh_domain.split(".")[0][::-1] - if response_token in r: - echoed_response = True - else: - echoed_response = False - - self.generic_ssrf.interactsh_subdomain_tags[subdomain_tag] = ( - event, - self.technique_description, - self.severity, - echoed_response, - ) - - -class Generic_SSRF(BaseSubmodule): - technique_description = "Generic SSRF (GET)" - severity = "HIGH" - - def set_base_url(self, event): - return event.data - - def create_paths(self): - test_paths = [] - for param in ssrf_params: - query_string = "" - subdomain_tag = self.generic_ssrf.helpers.rand_string(4) - ssrf_canary = f"{subdomain_tag}.{self.generic_ssrf.interactsh_domain}" - self.generic_ssrf.parameter_subdomain_tags_map[subdomain_tag] = param - query_string += f"{param}=http://{ssrf_canary}&" - test_paths.append((f"?{query_string.rstrip('&')}", subdomain_tag)) - return test_paths - - -class Generic_SSRF_POST(BaseSubmodule): - technique_description = "Generic SSRF (POST)" - severity = "HIGH" - - def set_base_url(self, event): - return event.data - - async def test(self, event): - test_url = f"{event.data}" - - post_data = {} - for param in ssrf_params: - subdomain_tag = self.generic_ssrf.helpers.rand_string(4, digits=False) - self.generic_ssrf.parameter_subdomain_tags_map[subdomain_tag] = param - post_data[param] = f"http://{subdomain_tag}.{self.generic_ssrf.interactsh_domain}" - - subdomain_tag_lower = self.generic_ssrf.helpers.rand_string(4, digits=False) - post_data_lower = { - k.lower(): f"http://{subdomain_tag_lower}.{self.generic_ssrf.interactsh_domain}" - for k, v in post_data.items() - } - - post_data_list = [(subdomain_tag, post_data), (subdomain_tag_lower, post_data_lower)] - - for tag, pd in post_data_list: - r = await self.generic_ssrf.helpers.curl(url=test_url, method="POST", post_data=pd) - self.process(event, r, tag) - - -class Generic_XXE(BaseSubmodule): - technique_description = "Generic XXE" - severity = "HIGH" - paths = None - - async def test(self, event): - rand_entity = self.generic_ssrf.helpers.rand_string(4, digits=False) - subdomain_tag = self.generic_ssrf.helpers.rand_string(4, digits=False) - - post_body = f""" - - -]> -&{rand_entity};""" - test_url = event.parsed_url.geturl() - r = await self.generic_ssrf.helpers.curl( - url=test_url, method="POST", raw_body=post_body, headers={"Content-type": "application/xml"} - ) - if r: - self.process(event, r, subdomain_tag) - - -class generic_ssrf(BaseModule): - watched_events = ["URL"] - produced_events = ["VULNERABILITY"] - flags = ["active", "aggressive", "web-thorough"] - meta = {"description": "Check for generic SSRFs", "created_date": "2022-07-30", "author": "@liquidsec"} - options = { - "skip_dns_interaction": False, - } - options_desc = { - "skip_dns_interaction": "Do not report DNS interactions (only HTTP interaction)", - } - in_scope_only = True - - deps_apt = ["curl"] - - async def setup(self): - self.submodules = {} - self.interactsh_subdomain_tags = {} - self.parameter_subdomain_tags_map = {} - self.severity = None - self.skip_dns_interaction = self.config.get("skip_dns_interaction", False) - - if self.scan.config.get("interactsh_disable", False) is False: - try: - self.interactsh_instance = self.helpers.interactsh() - self.interactsh_domain = await self.interactsh_instance.register(callback=self.interactsh_callback) - except InteractshError as e: - self.warning(f"Interactsh failure: {e}") - return False - else: - self.warning( - "The generic_ssrf module is completely dependent on interactsh to function, but it is disabled globally. Aborting." - ) - return None - - # instantiate submodules - for m in BaseSubmodule.__subclasses__(): - if m.__name__.startswith("Generic_"): - self.verbose(f"Starting generic_ssrf submodule: {m.__name__}") - self.submodules[m.__name__] = m(self) - - return True - - async def handle_event(self, event): - for s in self.submodules.values(): - await s.test(event) - - async def interactsh_callback(self, r): - protocol = r.get("protocol").upper() - if protocol == "DNS" and self.skip_dns_interaction: - return - - full_id = r.get("full-id", None) - subdomain_tag = full_id.split(".")[0] - - if full_id: - if "." in full_id: - match = self.interactsh_subdomain_tags.get(subdomain_tag) - if not match: - return - matched_event = match[0] - matched_technique = match[1] - matched_severity = match[2] - matched_echoed_response = str(match[3]) - - triggering_param = self.parameter_subdomain_tags_map.get(subdomain_tag, None) - description = f"Out-of-band interaction: [{matched_technique}]" - if triggering_param: - self.debug(f"Found triggering parameter: {triggering_param}") - description += f" [Triggering Parameter: {triggering_param}]" - description += f" [{protocol}] Echoed Response: {matched_echoed_response}" - - self.debug(f"Emitting event with description: {description}") # Debug the final description - - event_type = "VULNERABILITY" if protocol == "HTTP" else "FINDING" - event_data = { - "host": str(matched_event.host), - "url": matched_event.data, - "description": description, - } - if protocol == "HTTP": - event_data["severity"] = matched_severity - - await self.emit_event( - event_data, - event_type, - matched_event, - context=f"{{module}} scanned {matched_event.data} and detected {{event.type}}: {matched_technique}", - ) - else: - # this is likely caused by something trying to resolve the base domain first and can be ignored - self.debug("skipping result because subdomain tag was missing") - - async def cleanup(self): - if self.scan.config.get("interactsh_disable", False) is False: - try: - await self.interactsh_instance.deregister() - self.debug( - f"successfully deregistered interactsh session with correlation_id {self.interactsh_instance.correlation_id}" - ) - except InteractshError as e: - self.warning(f"Interactsh failure: {e}") - - async def finish(self): - if self.scan.config.get("interactsh_disable", False) is False: - await self.helpers.sleep(5) - try: - for r in await self.interactsh_instance.poll(): - await self.interactsh_callback(r) - except InteractshError as e: - self.debug(f"Error in interact.sh: {e}") diff --git a/bbot/modules/git.py b/bbot/modules/git.py index 569aa0e489..229adee35d 100644 --- a/bbot/modules/git.py +++ b/bbot/modules/git.py @@ -6,7 +6,7 @@ class git(BaseModule): watched_events = ["URL"] produced_events = ["FINDING", "CODE_REPOSITORY"] - flags = ["active", "safe", "web-basic", "code-enum"] + flags = ["safe", "active", "web", "code-enum"] meta = { "description": "Check for exposed .git repositories", "created_date": "2023-05-30", @@ -18,7 +18,7 @@ class git(BaseModule): fp_regex = re.compile(r"= self.max_scope_distance: diff --git a/bbot/modules/internal/dnsresolve.py b/bbot/modules/internal/dnsresolve.py index 3dddd289a4..f2472025c9 100644 --- a/bbot/modules/internal/dnsresolve.py +++ b/bbot/modules/internal/dnsresolve.py @@ -1,3 +1,4 @@ +import sys import ipaddress from contextlib import suppress @@ -37,6 +38,8 @@ async def setup(self): self.dns_search_distance = max(0, int(self.dns_config.get("search_distance", 1))) self._emit_raw_records = None + self.filter_ptrs = self.dns_config.get("filter_ptrs", True) + self.host_module = self.HostModule(self.scan) self.children_emitted = set() self.children_emitted_raw = set() @@ -59,17 +62,25 @@ async def handle_event(self, event, **kwargs): non_minimal_rdtypes = self.non_minimal_rdtypes # first, we find or create the main DNS_NAME or IP_ADDRESS associated with this event - main_host_event, whitelisted, blacklisted, new_event = self.get_dns_parent(event) + main_host_event, in_target, blacklisted, new_event = self.get_dns_parent(event) original_tags = set(event.tags) # minimal resolution - first, we resolve A/AAAA records for scope purposes if new_event or event is main_host_event: await self.resolve_event(main_host_event, types=minimal_rdtypes) - # are any of its IPs whitelisted/blacklisted? - whitelisted, blacklisted = self.check_scope(main_host_event) - if whitelisted and event.scope_distance > 0: - self.debug(f"Making {main_host_event} in-scope because it resolves to an in-scope resource (A/AAAA)") - main_host_event.scope_distance = 0 + # are any of its IPs in target scope or blacklisted? + in_target, blacklisted = self.check_scope(main_host_event) + if in_target and main_host_event.scope_distance > 0: + # when filter_ptrs is enabled, don't promote PTR-derived hostnames to in-scope + # this prevents rDNS results from triggering subdomain enumeration against unrelated domains + # (e.g. scanning 1.2.3.0/24 would otherwise enumerate every PTR parent like randomothercorp.com) + if self.filter_ptrs and "ptr" in main_host_event.tags: + self.debug(f"Not making {main_host_event} in-scope: PTR-derived hostname (filter_ptrs=true)") + else: + self.debug( + f"Making {main_host_event} in-scope because it resolves to an in-scope resource (A/AAAA)" + ) + main_host_event.scope_distance = 0 # abort if the event resolves to something blacklisted if blacklisted: @@ -99,9 +110,13 @@ async def handle_event(self, event, **kwargs): ) # if there weren't any DNS children and it's not an IP address, tag as unresolved + # Exception: don't convert seed events to DNS_NAME_UNRESOLVED so accept_seeds modules can process them if not main_host_event.raw_dns_records and not event_is_ip: - main_host_event.add_tag("unresolved") - main_host_event.type = "DNS_NAME_UNRESOLVED" + if "seed" not in main_host_event.tags: + main_host_event.add_tag("unresolved") + main_host_event.type = "DNS_NAME_UNRESOLVED" + # avoid emitting DNS_NAME_UNRESOLVED affiliates + main_host_event.always_emit_tags = [] # main_host_event.add_tag(f"resolve-distance-{main_host_event.dns_resolve_distance}") @@ -150,7 +165,7 @@ async def handle_wildcard_event(self, event): event.add_tag(f"{rdtype}-{wildcard_tag}") # wildcard event modification (www.evilcorp.com --> _wildcard.evilcorp.com) - if wildcard_rdtypes and "target" not in event.tags: + if wildcard_rdtypes and "seed" not in event.tags: # these are the rdtypes that have wildcards wildcard_rdtypes_set = set(wildcard_rdtypes) # consider the event a full wildcard if all its records are wildcards @@ -167,7 +182,9 @@ async def handle_wildcard_event(self, event): break wildcard_data = f"_wildcard.{wildcard_parent}" if wildcard_data != event.data: - self.debug(f'Wildcard detected, changing event.data "{event.data}" --> "{wildcard_data}"') + self.debug( + f'Wildcard detected, changing event.data "{event.pretty_string}" --> "{wildcard_data}"' + ) event.data = wildcard_data return True return False @@ -188,6 +205,10 @@ async def emit_dns_children(self, event): self.warning(f'Event validation failed for DNS child of {event}: "{child_host}" ({rdtype}): {e}') continue + # tag PTR-derived children so downstream logic can identify them + if rdtype == "PTR": + child_event.add_tag("ptr") + child_hash = hash(f"{event.host}:{module}:{child_host}") # if we haven't emitted this one before if child_hash not in self.children_emitted: @@ -219,21 +240,21 @@ async def emit_dns_children_raw(self, event, dns_tags): ) def check_scope(self, event): - whitelisted = False + in_target = False blacklisted = False dns_children = getattr(event, "dns_children", {}) for rdtype in ("A", "AAAA", "CNAME"): hosts = dns_children.get(rdtype, []) # update resolved hosts - event.resolved_hosts.update(hosts) + event.resolved_hosts.update(sys.intern(h) for h in hosts) for host in hosts: # having a CNAME to an in-scope host doesn't make you in-scope if rdtype != "CNAME": - if not whitelisted: + if not in_target: with suppress(ValidationError): - if self.scan.whitelisted(host): - whitelisted = True - event.add_tag(f"dns-whitelisted-{rdtype}") + if self.scan.in_target(host): + in_target = True + event.add_tag(f"dns-in-target-{rdtype}") # but a CNAME to a blacklisted host means you're blacklisted if not blacklisted: with suppress(ValidationError): @@ -242,8 +263,8 @@ def check_scope(self, event): event.add_tag("blacklisted") event.add_tag(f"dns-blacklisted-{rdtype}") if blacklisted: - whitelisted = False - return whitelisted, blacklisted + in_target = False + return in_target, blacklisted async def resolve_event(self, event, types): if not types: @@ -252,6 +273,7 @@ async def resolve_event(self, event, types): queries = [(event_host, rdtype) for rdtype in types] dns_errors = {} async for (query, rdtype), (answers, errors) in self.helpers.dns.resolve_raw_batch(queries): + rdtype = sys.intern(rdtype) # errors try: dns_errors[rdtype].update(errors) @@ -266,6 +288,8 @@ async def resolve_event(self, event, types): event.raw_dns_records[rdtype] = {answer} # hosts for _rdtype, host in extract_targets(answer): + _rdtype = sys.intern(_rdtype) + host = sys.intern(host) try: event.dns_children[_rdtype].add(host) except KeyError: @@ -287,16 +311,22 @@ async def resolve_event(self, event, types): def get_dns_parent(self, event): """ Get the first parent DNS_NAME / IP_ADDRESS of an event. If one isn't found, create it. + + Returns a 4-tuple of: + - the parent event + - whether the parent is in target + - whether the parent is blacklisted + - whether the parent is a new event, i.e. it is newly created or is the current event """ for parent in event.get_parents(include_self=True): if parent.host == event.host and parent.type in ("IP_ADDRESS", "DNS_NAME", "DNS_NAME_UNRESOLVED"): blacklisted = any(t.startswith("dns-blacklisted-") for t in parent.tags) - whitelisted = any(t.startswith("dns-whitelisted-") for t in parent.tags) + in_target = any(t.startswith("dns-in-target-") for t in parent.tags) new_event = parent is event - return parent, whitelisted, blacklisted, new_event + return parent, in_target, blacklisted, new_event tags = set() - if "target" in event.tags: - tags.add("target") + if "seed" in event.tags: + tags.add("seed") return ( self.scan.make_event( event.host, diff --git a/bbot/modules/internal/excavate.py b/bbot/modules/internal/excavate.py index f5c75a5c9b..5a0fc04d43 100644 --- a/bbot/modules/internal/excavate.py +++ b/bbot/modules/internal/excavate.py @@ -105,10 +105,12 @@ def extract_params_location(location_header_value, original_parsed_url): class YaraRuleSettings: - def __init__(self, description, tags, emit_match): + def __init__(self, description, tags, emit_match, severity, confidence): self.description = description self.tags = tags self.emit_match = emit_match + self.severity = severity + self.confidence = confidence class ExcavateRule: @@ -153,6 +155,8 @@ async def preprocess(self, r, event, discovery_context): description = "" tags = [] emit_match = False + severity = "INFO" + confidence = "UNKNOWN" if "description" in r.meta.keys(): description = r.meta["description"] @@ -160,8 +164,12 @@ async def preprocess(self, r, event, discovery_context): tags = self.excavate.helpers.chain_lists(r.meta["tags"]) if "emit_match" in r.meta.keys(): emit_match = True + if "severity" in r.meta.keys(): + severity = r.meta["severity"] + if "confidence" in r.meta.keys(): + confidence = r.meta["confidence"] - yara_rule_settings = YaraRuleSettings(description, tags, emit_match) + yara_rule_settings = YaraRuleSettings(description, tags, emit_match, severity, confidence) yara_results = {} for h in r.strings: yara_results[h.identifier.lstrip("$")] = sorted( @@ -185,7 +193,7 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte event : Event The event data associated with the YARA match. yara_rule_settings : YaraRuleSettings - The settings configured from YARA rule meta tags, including description, tags, and emit_match flag. + The settings configured from YARA rule meta tags, including description, severity, confidence, tags, and emit_match flag. discovery_context : DiscoveryContext The context in which the discovery is made. @@ -194,7 +202,10 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte """ for results in yara_results.values(): for result in results: - event_data = {"description": f"{discovery_context} {yara_rule_settings.description}"} + event_data = { + "name": f"{discovery_context} {yara_rule_settings.description}", + "description": f"{discovery_context} {yara_rule_settings.description}", + } if yara_rule_settings.emit_match: event_data["description"] += f" [{result}]" await self.report(event_data, event, yara_rule_settings, discovery_context) @@ -245,7 +256,7 @@ async def report( event : Event The parent event to which this event is related. yara_rule_settings : YaraRuleSettings - The settings configured from YARA rule meta tags, including description and tags. + The settings configured from YARA rule meta tags, including description, severity, confidence, and tags. discovery_context : DiscoveryContext The context in which the discovery is made. event_type : str, optional @@ -258,10 +269,16 @@ async def report( Returns: None """ - # If a description is not set and is needed, provide a basic one - if event_type == "FINDING" and "description" not in event_data.keys(): - event_data["description"] = f"{discovery_context} {yara_rule_settings['self.description']}" + if event_type == "FINDING": + if "description" not in event_data.keys(): + event_data["description"] = f"{discovery_context} {yara_rule_settings.description}" + if "name" not in event_data.keys(): + event_data["name"] = f"{discovery_context} {yara_rule_settings.description}" + if "severity" not in event_data.keys(): + event_data["severity"] = yara_rule_settings.severity + if "confidence" not in event_data.keys(): + event_data["confidence"] = yara_rule_settings.confidence subject = "" if isinstance(event_data, str): subject = f" {event_data}" @@ -281,7 +298,9 @@ def __init__(self, excavate): async def process(self, yara_results, event, yara_rule_settings, discovery_context): for identifier, results in yara_results.items(): for result in results: - event_data = {} + event_data = { + "name": f"Custom Yara Rule [{self.name}]", + } description_string = ( f" with description: [{yara_rule_settings.description}]" if yara_rule_settings.description else "" ) @@ -290,6 +309,9 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte ) if yara_rule_settings.emit_match: event_data["description"] += f" and extracted [{result}]" + event_data["severity"] = yara_rule_settings.get("severity", "LOW") + event_data["confidence"] = yara_rule_settings.get("confidence", "UNKNOWN") + await self.report(event_data, event, yara_rule_settings, discovery_context) @@ -306,7 +328,7 @@ class excavateTestRule(ExcavateRule): watched_events = ["HTTP_RESPONSE", "RAW_TEXT"] produced_events = ["URL_UNVERIFIED", "WEB_PARAMETER"] - flags = ["passive"] + flags = ["safe", "passive"] meta = { "description": "Passively extract juicy tidbits from scan data", "created_date": "2022-06-27", @@ -339,7 +361,7 @@ def in_bl(self, value): return True for bl_param_prefix in self.parameter_blacklist_prefixes: - if lower_value.startswith(bl_param_prefix.lower()): + if lower_value.startswith(bl_param_prefix): return True return False @@ -380,6 +402,7 @@ class GetJquery(ParameterExtractorRule): name = "GET jquery" discovery_regex = r"/\$.get\([^\)].+\)/ nocase" extraction_regex = re.compile(r"\$.get\([\'\"](.+)[\'\"].+(\{.+\})\)") + _json_key_regex = re.compile(r"(\w+):") output_type = "GETPARAM" async def extract(self): @@ -387,6 +410,8 @@ async def extract(self): if extracted_results: for action, extracted_parameters in extracted_results: extracted_parameters_dict = await self.convert_to_dict(extracted_parameters) + if extracted_parameters_dict is None: + continue for parameter_name, original_value in extracted_parameters_dict.items(): yield ( self.output_type, @@ -399,7 +424,7 @@ async def extract(self): async def convert_to_dict(self, extracted_str): extracted_str = extracted_str.replace("'", '"') extracted_str = await self.excavate.helpers.re.sub( - re.compile(r"(\w+):"), r'"\1":', extracted_str + self._json_key_regex, r'"\1":', extracted_str ) # Quote keys try: @@ -682,7 +707,7 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte class JWTExtractor(ExcavateRule): description = "Extracts JSON Web Tokens." yara_rules = { - "jwt": r'rule jwt { meta: emit_match = "True" description = "contains JSON Web Token (JWT)" strings: $jwt = /\beyJ[_a-zA-Z0-9\/+]*\.[_a-zA-Z0-9\/+]*\.[_a-zA-Z0-9\/+]*/ nocase condition: $jwt }', + "jwt": r'rule jwt { meta: emit_match = "True" description = "contains JSON Web Token (JWT)" confidence = "CONFIRMED" strings: $jwt = /\beyJ[_a-zA-Z0-9\/+]*\.[_a-zA-Z0-9\/+]*\.[_a-zA-Z0-9\/+]*/ nocase condition: $jwt }', } class ErrorExtractor(ExcavateRule): @@ -712,14 +737,15 @@ def __init__(self, excavate): signature_component_list.append(rf"${signature_name} = {signature}") signature_component = " ".join(signature_component_list) self.yara_rules["error_detection"] = ( - f'rule error_detection {{meta: description = "contains a verbose error message" strings: {signature_component} condition: any of them}}' + f'rule error_detection {{meta: description = "contains a verbose error message" severity = "INFO" confidence = "MEDIUM" strings: {signature_component} condition: any of them}}' ) async def process(self, yara_results, event, yara_rule_settings, discovery_context): for identifier in yara_results.keys(): for findings in yara_results[identifier]: event_data = { - "description": f"{discovery_context} {yara_rule_settings.description} ({identifier})" + "name": "Possible Verbose Error Message", + "description": f"{discovery_context} {yara_rule_settings.description} ({identifier})", } await self.report(event_data, event, yara_rule_settings, discovery_context, event_type="FINDING") @@ -743,21 +769,22 @@ def __init__(self, excavate): regexes_component_list.append(rf"${regex_name} = /\b{regex.pattern}/") regexes_component = " ".join(regexes_component_list) self.yara_rules["serialization_detection"] = ( - f'rule serialization_detection {{meta: description = "contains a possible serialized object" strings: {regexes_component} condition: any of them}}' + f'rule serialization_detection {{meta: description = "contains a possible serialized object" severity = "INFO" confidence = "MEDIUM" strings: {regexes_component} condition: any of them}}' ) async def process(self, yara_results, event, yara_rule_settings, discovery_context): for identifier in yara_results.keys(): for findings in yara_results[identifier]: event_data = { - "description": f"{discovery_context} {yara_rule_settings.description} ({identifier})" + "name": "Possible Serialized Object", + "description": f"{discovery_context} {yara_rule_settings.description} ({identifier})", } await self.report(event_data, event, yara_rule_settings, discovery_context, event_type="FINDING") class FunctionalityExtractor(ExcavateRule): description = "Detects potentially exploitable functionality and attack surface in web applications." yara_rules = { - "File_Upload_Functionality": r'rule File_Upload_Functionality { meta: description = "contains file upload functionality" strings: $fileuploadfunc = /]+type=["\']?file["\']?[^>]+>/ nocase condition: $fileuploadfunc }', + "File_Upload_Functionality": r'rule File_Upload_Functionality { meta: description = "contains file upload functionality" confidence = "CONFIRMED" strings: $fileuploadfunc = /]+type=["\']?file["\']?[^>]+>/ nocase condition: $fileuploadfunc }', "Web_Service_WSDL": r'rule Web_Service_WSDL { meta: emit_match = "True" description = "contains a web service WSDL URL" strings: $wsdl = /https?:\/\/[^\s]*\.(wsdl)/ nocase condition: $wsdl }', } @@ -790,13 +817,30 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte except ValueError as e: self.excavate.debug(f"Failed to parse netloc: {e}") continue + # convert websocket URLs to their HTTP equivalents + if parsed_url.scheme in ["ws", "wss"]: + http_scheme = "https" if parsed_url.scheme == "wss" else "http" + http_url = parsed_url._replace(scheme=http_scheme).geturl() + await self.report( + http_url, + event, + yara_rule_settings, + discovery_context, + event_type="URL_UNVERIFIED", + ) + continue + if parsed_url.scheme in ["http", "https"]: continue def abort_if(e): return e.scope_distance > 0 - finding_data = {"host": str(host), "description": f"Non-HTTP URI: {parsed_url.geturl()}"} + finding_data = { + "host": str(host), + "name": "Non-HTTP URI", + "description": f"Non-HTTP URI: {parsed_url.geturl()}", + } await self.report(finding_data, event, yara_rule_settings, discovery_context, abort_if=abort_if) protocol_data = {"protocol": parsed_url.scheme, "host": str(host)} if port: @@ -1015,7 +1059,9 @@ async def setup(self): self.add_yara_rule(rule_name, rule_content, excavateRule) self.parameter_blacklist = set(p.lower() for p in self.scan.config.get("parameter_blacklist", [])) - self.parameter_blacklist_prefixes = set(self.scan.config.get("parameter_blacklist_prefixes", [])) + self.parameter_blacklist_prefixes = set( + p.lower() for p in self.scan.config.get("parameter_blacklist_prefixes", []) + ) self.custom_yara_rules = str(self.config.get("custom_yara_rules", "")) if self.custom_yara_rules: @@ -1092,7 +1138,7 @@ async def search(self, data, event, content_type, discovery_context="HTTP respon param_type="SPECULATIVE", name=parameter_name, original_value=original_value, - url=str(event.data["url"]), + url=event.url, description=f"HTTP Extracted Parameter (speculative from {source_type} content) [{parameter_name}]", additional_params={}, event=event, @@ -1135,8 +1181,8 @@ async def handle_event(self, event, **kwargs): await self.emit_custom_parameters(event, "http_cookies", "COOKIE", "Custom Cookie") await self.emit_custom_parameters(event, "http_headers", "HEADER", "Custom Header") - # if parameter extraction is enabled, and querystring removal is disabled, and the event is directly from the TARGET, create a WEB - if self.url_querystring_remove is False and str(event.parent.parent.module) == "TARGET": + # if parameter extraction is enabled, and querystring removal is disabled, and the event is directly from the SEED, create a WEB + if self.url_querystring_remove is False and str(event.parent.parent.module) == "SEED": self.debug(f"Processing target URL [{urlunparse(event.parsed_url)}] for GET parameters") for ( method, @@ -1212,7 +1258,7 @@ async def handle_event(self, event, **kwargs): reported_location_header = True await self.emit_event( url_event, - context=f'excavate looked in "Location" header and found {url_event.type}: {url_event.data}', + context=f'excavate looked in "Location" header and found {url_event.type}: {url_event.url}', ) # Try to extract parameters from the redirect URL @@ -1243,11 +1289,11 @@ async def handle_event(self, event, **kwargs): content_type = headers["content-type"][0] # skip PDF responses -- running YARA/regex on raw PDF bytes produces false positives and wastes time. - # PDFs are still processed correctly via the filedownload → extractous → RAW_TEXT pipeline, + # PDFs are still processed correctly via the filedownload → kreuzberg → RAW_TEXT pipeline, # which extracts readable text and feeds it back to excavate as a RAW_TEXT event (handled separately below). # TODO: remove this in favor of a proper categorization system for text vs non-text (i.e. to-be-extracted) content if content_type and "application/pdf" in content_type.lower(): - self.debug(f"Skipping PDF response: {event.data.get('url', 'unknown')}") + self.debug(f"Skipping PDF response: {event.url or 'unknown'}") return await self.search( diff --git a/bbot/modules/internal/speculate.py b/bbot/modules/internal/speculate.py index 45f3c6a6f0..0e31e9158b 100644 --- a/bbot/modules/internal/speculate.py +++ b/bbot/modules/internal/speculate.py @@ -25,23 +25,23 @@ class speculate(BaseInternalModule): "USERNAME", ] produced_events = ["DNS_NAME", "OPEN_TCP_PORT", "IP_ADDRESS", "FINDING", "ORG_STUB"] - flags = ["passive"] + flags = ["safe", "passive"] meta = { "description": "Derive certain event types from others by common sense", "created_date": "2022-05-03", "author": "@liquidsec", } - options = {"max_hosts": 65536, "ports": "80,443", "essential_only": False} + options = {"ip_range_max_hosts": 65536, "ports": "80,443", "essential_only": False} options_desc = { - "max_hosts": "Max number of IP_RANGE hosts to convert into IP_ADDRESS events", + "ip_range_max_hosts": "Max number of hosts an IP_RANGE can contain to allow conversion into IP_ADDRESS events", "ports": "The set of ports to speculate on", "essential_only": "Only enable essential speculate features (no extra discovery)", } scope_distance_modifier = 1 _priority = 4 - default_discovery_context = "speculated {event.type}: {event.data}" + default_discovery_context = "speculated {event.type}: {event.pretty_string}" async def setup(self): scan_modules = [m for m in self.scan.modules.values() if m._type == "scan"] @@ -64,16 +64,6 @@ async def setup(self): if not self.portscanner_enabled: self.info(f"No portscanner enabled. Assuming open ports: {', '.join(str(x) for x in self.ports)}") - - target_len = len(self.scan.target.seeds) - if target_len > self.config.get("max_hosts", 65536): - if not self.portscanner_enabled: - self.hugewarning( - f"Selected target ({target_len:,} hosts) is too large, skipping IP_RANGE --> IP_ADDRESS speculation" - ) - self.hugewarning('Enabling the "portscan" module is highly recommended') - self.range_to_ip = False - return True async def handle_event(self, event): @@ -86,8 +76,17 @@ async def handle_event(self, event): speculate_open_ports = self.emit_open_ports and event_in_scope_distance # generate individual IP addresses from IP range - if event.type == "IP_RANGE" and self.range_to_ip: + if event.type == "IP_RANGE": net = ipaddress.ip_network(event.data) + num_ips = net.num_addresses + ip_range_max_hosts = self.config.get("ip_range_max_hosts", 65536) + + if num_ips > ip_range_max_hosts: + self.warning( + f"IP range {event.pretty_string} contains {num_ips:,} addresses, which exceeds ip_range_max_hosts limit of {ip_range_max_hosts:,}. Skipping IP_ADDRESS speculation." + ) + return + ips = list(net) random.shuffle(ips) for ip in ips: @@ -114,7 +113,7 @@ async def handle_event(self, event): "OPEN_TCP_PORT", parent=event, internal=True, - context="speculated {event.type}: {event.data}", + context="speculated {event.type}: {event.pretty_string}", ) ### END ESSENTIAL SPECULATION ### @@ -126,7 +125,7 @@ async def handle_event(self, event): parent = self.helpers.parent_domain(event.host_original) if parent != event.data: await self.emit_event( - parent, "DNS_NAME", parent=event, context="speculated parent {event.type}: {event.data}" + parent, "DNS_NAME", parent=event, context="speculated parent {event.type}: {event.pretty_string}" ) # URL --> OPEN_TCP_PORT @@ -139,37 +138,36 @@ async def handle_event(self, event): "OPEN_TCP_PORT", parent=event, internal=not event_is_url, # if the URL is verified, the port is definitely open - context=f"speculated {{event.type}} from {event.type}: {{event.data}}", + context=f"speculated {{event.type}} from {event.type}: {{event.pretty_string}}", ) # speculate sub-directory URLS from URLS if event.type == "URL": - url_parents = self.helpers.url_parents(event.data) + url_parents = self.helpers.url_parents(event.url) for up in url_parents: url_event = self.make_event(f"{up}/", "URL_UNVERIFIED", parent=event) if url_event is not None: # inherit web spider distance from parent (don't increment) parent_web_spider_distance = getattr(event, "web_spider_distance", 0) url_event.web_spider_distance = parent_web_spider_distance - await self.emit_event(url_event, context="speculated web sub-directory {event.type}: {event.data}") + await self.emit_event( + url_event, context="speculated web sub-directory {event.type}: {event.pretty_string}" + ) # speculate URL_UNVERIFIED from URL or any event with "url" attribute event_is_url = event.type == "URL" - event_has_url = isinstance(event.data, dict) and "url" in event.data + event_has_url = not event.type.startswith("URL") and isinstance(event.data, dict) and "url" in event.data event_tags = ["httpx-safe"] if event.type in ("CODE_REPOSITORY", "SOCIAL") else [] if event_is_url or event_has_url: - if event_is_url: - url = event.data - else: - url = event.data["url"] + url = event.url # only emit the url if it's not already in the event's history - if not any(e.type == "URL_UNVERIFIED" and e.data == url for e in event.get_parents()): + if not any(e.type == "URL_UNVERIFIED" and e.url == url for e in event.get_parents()): await self.emit_event( url, "URL_UNVERIFIED", tags=event_tags, parent=event, - context="speculated {event.type}: {event.data}", + context="speculated {event.type}: {event.pretty_string}", ) # ORG_STUB from TLD, SOCIAL, AZURE_TENANT @@ -196,7 +194,7 @@ async def handle_event(self, event): self.org_stubs_seen.add(stub_hash) stub_event = self.make_event(stub, "ORG_STUB", parent=event) if stub_event: - await self.emit_event(stub_event, context="speculated {event.type}: {event.data}") + await self.emit_event(stub_event, context="speculated {event.type}: {event.pretty_string}") # USERNAME --> EMAIL if event.type == "USERNAME": @@ -204,4 +202,4 @@ async def handle_event(self, event): if validators.soft_validate(email, "email"): email_event = self.make_event(email, "EMAIL_ADDRESS", parent=event, tags=["affiliate"]) if email_event: - await self.emit_event(email_event, context="detected {event.type}: {event.data}") + await self.emit_event(email_event, context="detected {event.type}: {event.pretty_string}") diff --git a/bbot/modules/internal/unarchive.py b/bbot/modules/internal/unarchive.py index 3cba8ef9ab..3b3162c947 100644 --- a/bbot/modules/internal/unarchive.py +++ b/bbot/modules/internal/unarchive.py @@ -7,7 +7,7 @@ class unarchive(BaseInternalModule): watched_events = ["FILESYSTEM"] produced_events = ["FILESYSTEM"] - flags = ["passive", "safe"] + flags = ["safe", "passive"] meta = { "description": "Extract different types of files into folders on the filesystem", "created_date": "2024-12-08", diff --git a/bbot/modules/ip2location.py b/bbot/modules/ip2location.py index 2a4b387f45..80625f2852 100644 --- a/bbot/modules/ip2location.py +++ b/bbot/modules/ip2location.py @@ -8,7 +8,7 @@ class IP2Location(BaseModule): watched_events = ["IP_ADDRESS"] produced_events = ["GEOLOCATION"] - flags = ["passive", "safe"] + flags = ["safe", "passive"] meta = { "description": "Query IP2location.io's API for geolocation information. ", "created_date": "2023-09-12", @@ -51,7 +51,7 @@ async def handle_event(self, event): else: self.verbose(f"No response from {url}") except Exception: - self.verbose(f"Error retrieving results for {event.data}", trace=True) + self.verbose(f"Error retrieving results for {event.pretty_string}", trace=True) return geo_data = {k: v for k, v in geo_data.items() if v is not None} @@ -70,5 +70,5 @@ async def handle_event(self, event): geo_data, "GEOLOCATION", event, - context=f'{{module}} queried IP2Location API for "{event.data}" and found {{event.type}}: {description}', + context=f'{{module}} queried IP2Location API for "{event.pretty_string}" and found {{event.type}}: {description}', ) diff --git a/bbot/modules/ipneighbor.py b/bbot/modules/ipneighbor.py index 3bae28a37f..f5f1007b00 100644 --- a/bbot/modules/ipneighbor.py +++ b/bbot/modules/ipneighbor.py @@ -6,7 +6,7 @@ class ipneighbor(BaseModule): watched_events = ["IP_ADDRESS"] produced_events = ["IP_ADDRESS"] - flags = ["passive", "subdomain-enum", "aggressive"] + flags = ["passive", "subdomain-enum", "loud"] meta = { "description": "Look beside IPs in their surrounding subnet", "created_date": "2022-06-08", @@ -39,5 +39,5 @@ async def handle_event(self, event): if ip_event: await self.emit_event( ip_event, - context="{module} produced {event.type}: {event.data}", + context="{module} produced {event.type}: {event.pretty_string}", ) diff --git a/bbot/modules/ipstack.py b/bbot/modules/ipstack.py index 02cfe0f3dc..6a00935f96 100644 --- a/bbot/modules/ipstack.py +++ b/bbot/modules/ipstack.py @@ -9,7 +9,7 @@ class Ipstack(BaseModule): watched_events = ["IP_ADDRESS"] produced_events = ["GEOLOCATION"] - flags = ["passive", "safe"] + flags = ["safe", "passive"] meta = { "description": "Query IPStack's GeoIP API", "created_date": "2022-11-26", @@ -30,7 +30,7 @@ async def setup(self): async def handle_event(self, event): try: - url = f"{self.base_url}/{event.data}?access_key={{api_key}}" + url = f"{self.base_url}/{event.pretty_string}?access_key={{api_key}}" result = await self.api_request(url) if result: geo_data = result.json() @@ -39,7 +39,7 @@ async def handle_event(self, event): else: self.verbose(f"No response from {url}") except Exception: - self.verbose(f"Error retrieving results for {event.data}", trace=True) + self.verbose(f"Error retrieving results for {event.pretty_string}", trace=True) return geo_data = {k: v for k, v in geo_data.items() if v is not None} if "error" in geo_data: @@ -57,5 +57,5 @@ async def handle_event(self, event): geo_data, "GEOLOCATION", event, - context=f'{{module}} queried ipstack.com\'s API for "{event.data}" and found {{event.type}}: {description}', + context=f'{{module}} queried ipstack.com\'s API for "{event.pretty_string}" and found {{event.type}}: {description}', ) diff --git a/bbot/modules/jadx.py b/bbot/modules/jadx.py index 33722fc3d7..d2f8437af6 100644 --- a/bbot/modules/jadx.py +++ b/bbot/modules/jadx.py @@ -6,7 +6,7 @@ class jadx(BaseModule): watched_events = ["FILESYSTEM"] produced_events = ["FILESYSTEM"] - flags = ["passive", "safe", "code-enum"] + flags = ["safe", "passive", "code-enum"] meta = { "description": "Decompile APKs and XAPKs using JADX", "created_date": "2024-11-04", diff --git a/bbot/modules/extractous.py b/bbot/modules/kreuzberg.py similarity index 72% rename from bbot/modules/extractous.py rename to bbot/modules/kreuzberg.py index c37be08008..373168180c 100644 --- a/bbot/modules/extractous.py +++ b/bbot/modules/kreuzberg.py @@ -1,12 +1,13 @@ -from extractous import Extractor +import pypdfium2 +from kreuzberg import extract_file from bbot.modules.base import BaseModule -class extractous(BaseModule): +class kreuzberg(BaseModule): watched_events = ["FILESYSTEM"] produced_events = ["RAW_TEXT"] - flags = ["passive", "safe"] + flags = ["safe", "passive"] meta = { "description": "Module to extract data from files", "created_date": "2024-06-03", @@ -65,7 +66,7 @@ class extractous(BaseModule): "extensions": "File extensions to parse", } - deps_pip = ["extractous~=0.3.0"] + deps_pip = ["kreuzberg>=4.3,<4.5", "pypdfium2~=5.0"] scope_distance_modifier = 1 async def setup(self): @@ -82,11 +83,17 @@ async def filter_event(self, event): async def handle_event(self, event): file_path = event.data["path"] - content = await self.scan.helpers.run_in_executor_mp(extract_text, file_path) - if isinstance(content, tuple): - error, traceback = content - self.error(f"Error extracting text from {file_path}: {error}") - self.trace(traceback) + try: + if file_path.lower().endswith(".pdf"): + content = await self.helpers.run_in_executor_mp(self.extract_pdf, file_path) + else: + result = await extract_file(file_path) + content = result.content.strip() + except Exception as e: + import traceback + + self.error(f"Error extracting text from {file_path}: {e}") + self.trace(traceback.format_exc()) return if content: @@ -98,27 +105,22 @@ async def handle_event(self, event): ) await self.emit_event(raw_text_event) + @staticmethod + def extract_pdf(file_path): + """Extract text from PDF using pypdfium2 directly instead of kreuzberg. -def extract_text(file_path): - """ - extract_text Extracts plaintext from a document path using extractous. - - :param file_path: The path of the file to extract text from. - :return: ASCII-encoded plaintext extracted from the document. - """ - - try: - extractor = Extractor() - reader, metadata = extractor.extract_file(str(file_path)) - - result = "" - buffer = reader.read(4096) - while len(buffer) > 0: - result += buffer.decode("utf-8", errors="ignore") - buffer = reader.read(4096) - - return result.strip() - except Exception as e: - import traceback - - return (str(e), traceback.format_exc()) + kreuzberg's bundled pdfium extracts text spatially via get_text_bounded(), which + clips long unbroken strings (JWTs, base64, URLs) that extend beyond the page width. + pypdfium2's get_text_range() extracts by character index, returning complete text + regardless of spatial position. + """ + document = pypdfium2.PdfDocument(file_path) + try: + pages = [] + for page in document: + textpage = page.get_textpage() + text = textpage.get_text_range() + pages.append(text) + return " ".join("\n".join(pages).strip().split()) + finally: + document.close() diff --git a/bbot/modules/leakix.py b/bbot/modules/leakix.py index ac9e81f87b..1f79dc8b12 100644 --- a/bbot/modules/leakix.py +++ b/bbot/modules/leakix.py @@ -4,7 +4,7 @@ class leakix(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] options = {"api_key": ""} # NOTE: API key is not required (but having one will get you more results) options_desc = {"api_key": "LeakIX API Key"} diff --git a/bbot/modules/deadly/legba.py b/bbot/modules/legba.py similarity index 98% rename from bbot/modules/deadly/legba.py rename to bbot/modules/legba.py index 2c62a77032..27b8a6b4bd 100644 --- a/bbot/modules/deadly/legba.py +++ b/bbot/modules/legba.py @@ -18,7 +18,7 @@ def map_protocol_to_legba_plugin_name(common_protocol_name: str) -> str: class legba(BaseModule): watched_events = ["PROTOCOL"] produced_events = ["FINDING"] - flags = ["active", "aggressive", "deadly"] + flags = ["active", "loud", "invasive"] per_hostport_only = True meta = { "description": "Credential bruteforcing supporting various services.", @@ -133,6 +133,7 @@ async def parse_output(self, output_filepath, event): "confidence": "CONFIRMED", "host": str(event.host), "port": str(event.port), + "name": f"Legba - {protocol.upper()} Credentials", "description": f"Valid {protocol} credentials found - {message_addition}", }, "FINDING", diff --git a/bbot/modules/lightfuzz/lightfuzz.py b/bbot/modules/lightfuzz/lightfuzz.py index 6d114b827b..231081006f 100644 --- a/bbot/modules/lightfuzz/lightfuzz.py +++ b/bbot/modules/lightfuzz/lightfuzz.py @@ -6,12 +6,12 @@ class lightfuzz(BaseModule): watched_events = ["URL", "WEB_PARAMETER"] - produced_events = ["FINDING", "VULNERABILITY"] - flags = ["active", "aggressive", "web-thorough", "deadly"] + produced_events = ["FINDING"] + flags = ["active", "loud", "web-heavy", "invasive"] options = { "force_common_headers": False, - "enabled_submodules": ["sqli", "cmdi", "xss", "path", "ssti", "crypto", "serial", "esi"], + "enabled_submodules": ["sqli", "cmdi", "xss", "path", "ssti", "crypto", "serial", "esi", "ssrf"], "disable_post": False, "try_post_as_get": False, "try_get_as_post": False, @@ -80,15 +80,24 @@ async def interactsh_callback(self, r): details = self.interactsh_subdomain_tags.get(full_id.split(".")[0]) if not details["event"]: return - # currently, this is only used by the cmdi submodule. Later, when other modules use it, we will need to store description data in the interactsh_subdomain_tags dictionary + protocol = r.get("protocol", "dns").lower() + severity = details.get("severity", "HIGH") + confidence = details.get("confidence", "CONFIRMED") + # Allow submodules to specify alternative severity/confidence for DNS-only interactions + if protocol == "dns": + severity = details.get("severity_dns", severity) + confidence = details.get("confidence_dns", confidence) + description = f"{details['description']} Interaction Protocol: [{protocol}]" await self.emit_event( { - "severity": "CRITICAL", + "severity": severity, + "confidence": confidence, "host": str(details["event"].host), - "url": details["event"].data["url"], - "description": f"OS Command Injection (OOB Interaction) Type: [{details['type']}] Parameter Name: [{details['name']}] Probe: [{details['probe']}]", + "url": details["event"].url, + "name": f"Lightfuzz - {details['name']}", + "description": description, }, - "VULNERABILITY", + "FINDING", details["event"], ) else: @@ -100,7 +109,7 @@ def _outgoing_dedup_hash(self, event): ( "lightfuzz", str(event.host), - event.data["url"], + event.url, event.data["description"], event.data.get("type", ""), event.data.get("name", ""), @@ -112,7 +121,12 @@ async def run_submodule(self, submodule, event): await submodule_instance.fuzz() if len(submodule_instance.results) > 0: for r in submodule_instance.results: - event_data = {"host": str(event.host), "url": event.data["url"], "description": r["description"]} + event_data = { + "host": str(event.host), + "url": event.url, + "name": r["name"], + "description": r["description"], + } envelopes = getattr(event, "envelopes", None) envelope_summary = getattr(envelopes, "summary", None) @@ -120,11 +134,12 @@ async def run_submodule(self, submodule, event): # Append the envelope summary to the description event_data["description"] += f" Envelopes: [{envelope_summary}]" - if r["type"] == "VULNERABILITY": - event_data["severity"] = r["severity"] + event_data["severity"] = r["severity"] + event_data["confidence"] = r["confidence"] + event_data["name"] = f"Lightfuzz - {r['name']}" await self.emit_event( event_data, - r["type"], + "FINDING", event, ) @@ -141,14 +156,14 @@ async def handle_event(self, event): "type": "HEADER", "name": h, "original_value": None, - "url": event.data, + "url": event.url, "description": description, } await self.emit_event(data, "WEB_PARAMETER", event) elif event.type == "WEB_PARAMETER": # check connectivity to url - connectivity_test = await self.helpers.request(event.data["url"], timeout=10) + connectivity_test = await self.helpers.request(event.url, timeout=10) if connectivity_test: original_type = event.data["type"] @@ -175,7 +190,7 @@ async def handle_event(self, event): self.debug(f"Starting {submodule_name} fuzz() (try_get_as_post)") await self.run_submodule(submodule, event) else: - self.debug(f"WEB_PARAMETER URL {event.data['url']} failed connectivity test, aborting") + self.debug(f"WEB_PARAMETER URL {event.url} failed connectivity test, aborting") async def cleanup(self): if self.interactsh_instance: @@ -189,9 +204,15 @@ async def cleanup(self): async def finish(self): if self.interactsh_instance: + self.debug("finish(): sleeping 5s before final interactsh poll") await self.helpers.sleep(5) try: - for r in await self.interactsh_instance.poll(): + results = await self.interactsh_instance.poll() + self.debug(f"finish(): interactsh poll returned {len(results)} interaction(s)") + for r in results: + protocol = r.get("protocol", "unknown") + full_id = r.get("full-id", "unknown") + self.debug(f"finish(): interactsh interaction: protocol={protocol}, full-id={full_id}") await self.interactsh_callback(r) except InteractshError as e: self.debug(f"Error in interact.sh: {e}") diff --git a/bbot/modules/lightfuzz/submodules/base.py b/bbot/modules/lightfuzz/submodules/base.py index 171e78c613..d5730aee30 100644 --- a/bbot/modules/lightfuzz/submodules/base.py +++ b/bbot/modules/lightfuzz/submodules/base.py @@ -27,7 +27,7 @@ def is_base64(s): try: if base64.b64encode(base64.b64decode(s)).decode() == s: return True - except (binascii.Error, UnicodeDecodeError): + except (binascii.Error, UnicodeDecodeError, ValueError): return False return False @@ -65,7 +65,7 @@ def conditional_urlencode(self, probe, event_type, skip_urlencoding=False): def build_query_string(self, probe, parameter_name, additional_params=None): """Constructs a URL with query parameters from the given probe and additional parameters.""" - url = f"{self.event.data['url']}?{parameter_name}={probe}" + url = f"{self.event.url}?{parameter_name}={probe}" if additional_params: url = self.lightfuzz.helpers.add_get_params(url, additional_params, encode=False).geturl() return url @@ -106,19 +106,19 @@ def prepare_request( return {"method": "GET", "cookies": cookies, "url": url} elif event_type == "COOKIE": cookies_probe = {parameter_name: probe} - return {"method": "GET", "cookies": {**cookies, **cookies_probe}, "url": self.event.data["url"]} + return {"method": "GET", "cookies": {**cookies, **cookies_probe}, "url": self.event.url} elif event_type == "HEADER": headers = {parameter_name: probe} - return {"method": "GET", "headers": headers, "cookies": cookies, "url": self.event.data["url"]} + return {"method": "GET", "headers": headers, "cookies": cookies, "url": self.event.url} elif event_type in ["POSTPARAM", "BODYJSON"]: # Prepare data for POSTPARAM and BODYJSON event types data = {parameter_name: probe} if additional_params: data.update(additional_params) if event_type == "BODYJSON": - return {"method": "POST", "json": data, "cookies": cookies, "url": self.event.data["url"]} + return {"method": "POST", "json": data, "cookies": cookies, "url": self.event.url} else: - return {"method": "POST", "data": data, "cookies": cookies, "url": self.event.data["url"]} + return {"method": "POST", "data": data, "cookies": cookies, "url": self.event.url} def compare_baseline( self, @@ -167,7 +167,7 @@ async def baseline_probe(self, cookies): return await self.lightfuzz.helpers.request( method=method, cookies=cookies, - url=self.event.data.get("url"), + url=self.event.url, allow_redirects=False, retries=1, timeout=10, diff --git a/bbot/modules/lightfuzz/submodules/cmdi.py b/bbot/modules/lightfuzz/submodules/cmdi.py index 11576f1dc5..dcc3d3a481 100644 --- a/bbot/modules/lightfuzz/submodules/cmdi.py +++ b/bbot/modules/lightfuzz/submodules/cmdi.py @@ -64,7 +64,7 @@ async def fuzz(self): self.debug(f"canary [{canary}] found in response when sending probe [{p}]") if p == "AAAA": # Handle detection false positive probe self.warning( - f"False Postive Probe appears to have been triggered for {self.event.data['url']}, aborting remaining detection" + f"False Postive Probe appears to have been triggered for {self.event.url}, aborting remaining detection" ) return positive_detections.append(p) # Add detected probes to positive detections @@ -74,22 +74,25 @@ async def fuzz(self): if len(positive_detections) > 0: self.results.append( { - "type": "FINDING", + "name": "Possible Command Injection", + "severity": "CRITICAL", + "confidence": "MEDIUM", "description": f"POSSIBLE OS Command Injection. {self.metadata()} Detection Method: [echo canary] CMD Probe Delimeters: [{' '.join(positive_detections)}]", } ) # Blind OS Command Injection if self.lightfuzz.interactsh_instance: - self.lightfuzz.event_dict[self.event.data["url"]] = self.event # Store the event associated with the URL + self.lightfuzz.event_dict[self.event.url] = self.event # Store the event associated with the URL for p in cmdi_probe_strings: # generate a random subdomain tag and associate it with the event, type, name, and probe subdomain_tag = self.lightfuzz.helpers.rand_string(4, digits=False) self.lightfuzz.interactsh_subdomain_tags[subdomain_tag] = { "event": self.event, - "type": self.event.data["type"], - "name": self.event.data["name"], - "probe": p, + "name": "OS Command Injection", + "description": f"OS Command Injection (OOB Interaction) Type: [{self.event.data['type']}] Parameter Name: [{self.event.data['name']}] Probe: [{p}]", + "severity": "CRITICAL", + "confidence": "CONFIRMED", } # payload is an nslookup command that includes the interactsh domain prepended the previously generated subdomain tag interactsh_probe = f"{p} nslookup {subdomain_tag}.{self.lightfuzz.interactsh_domain} {p}" diff --git a/bbot/modules/lightfuzz/submodules/crypto.py b/bbot/modules/lightfuzz/submodules/crypto.py index 6c3e060640..b91069a736 100644 --- a/bbot/modules/lightfuzz/submodules/crypto.py +++ b/bbot/modules/lightfuzz/submodules/crypto.py @@ -83,6 +83,22 @@ def compiled_rules(self): _compiled_rules_cache = self.lightfuzz.helpers.yara.compile_strings(self.crypto_error_strings, nocase=True) return _compiled_rules_cache + @staticmethod + def is_plausible_base64_crypto(s): + """ + Check if a string is plausibly base64-encoded cryptographic data. + Non-standard encodings like F5 BIG-IP's A-P nibble encoding use a narrow + consecutive character range that technically round-trips as base64 but is + not actual base64. Real base64 of encrypted/random bytes spans a wide + character range (typically 70+). + """ + unique_chars = set(s) - set("=") + if len(s) >= 16 and unique_chars: + char_ords = [ord(c) for c in unique_chars] + if max(char_ords) - min(char_ords) <= 20: + return False + return True + @staticmethod def format_agnostic_decode(input_string, urldecode=False): """ @@ -101,7 +117,7 @@ def format_agnostic_decode(input_string, urldecode=False): if BaseLightfuzz.is_hex(input_string): data = bytes.fromhex(input_string) encoding = "hex" - elif BaseLightfuzz.is_base64(input_string): + elif BaseLightfuzz.is_base64(input_string) and crypto.is_plausible_base64_crypto(input_string): data = base64.b64decode(input_string) encoding = "base64" else: @@ -300,11 +316,25 @@ async def padding_oracle(self, probe_value, cookies): ) if padding_oracle_result is True: + # Confirmation round: re-run to rule out jitter-based false positives + self.debug(f"Initial padding oracle detection for block_size={block_size}, running confirmation round") + confirmation_result = await self.padding_oracle_execute(data, encoding, block_size, cookies) + if confirmation_result is None: + confirmation_result = await self.padding_oracle_execute( + data, encoding, block_size, cookies, possible_first_byte=False + ) + if confirmation_result is not True: + self.debug( + f"Confirmation round failed for block_size={block_size} - likely jitter false positive, suppressing" + ) + continue + context = f"Lightfuzz Cryptographic Probe Submodule detected a probable padding oracle vulnerability after manipulating parameter: [{self.event.data['name']}]" self.results.append( { - "type": "VULNERABILITY", "severity": "HIGH", + "name": "Padding Oracle Vulnerability", + "confidence": "HIGH", "description": f"Padding Oracle Vulnerability. Block size: [{str(block_size)}] {self.metadata()}", "context": context, } @@ -338,7 +368,9 @@ async def error_string_search(self, text_dict, baseline_text): if unique_matches: self.results.append( { - "type": "FINDING", + "name": "Possible Cryptographic Error", + "severity": "INFO", + "confidence": "LOW", "description": f"Possible Cryptographic Error. {self.metadata()} Strings: [{','.join(unique_matches)}] Detection Technique(s): [{','.join(matching_techniques)}]", "context": context, } @@ -377,7 +409,7 @@ async def fuzz(self): # obtain the baseline probe to compare against baseline_probe = await self.baseline_probe(cookies) if not baseline_probe: - self.verbose(f"Couldn't get baseline_probe for url {self.event.data['url']}, aborting") + self.verbose(f"Couldn't get baseline_probe for url {self.event.url}, aborting") return # perform the manipulation techniques @@ -432,7 +464,9 @@ async def fuzz(self): context = f"Lightfuzz Cryptographic Probe Submodule detected a parameter ({self.event.data['name']}) to appears to drive a cryptographic operation" self.results.append( { - "type": "FINDING", + "name": "Probable Cryptographic Parameter", + "severity": "INFO", + "confidence": "LOW", "description": f"Probable Cryptographic Parameter. {self.metadata()} Detection Technique(s): [{', '.join(confirmed_techniques)}]", "context": context, } @@ -469,6 +503,8 @@ async def fuzz(self): ): # for each additional parameter, we send a probe and check if it causes the same change in the response as the original probe for additional_param_name, additional_param_value in self.event.data["additional_params"].items(): + if additional_param_value is None: + continue try: additional_param_probe = await self.compare_probe( http_compare, @@ -486,7 +522,9 @@ async def fuzz(self): context = f"Lightfuzz Cryptographic Probe Submodule detected a parameter ({self.event.data['name']}) that is a likely a hash, which is connected to another parameter {additional_param_name})" self.results.append( { - "type": "FINDING", + "name": "Possible Length Extension Attack", + "severity": "INFO", + "confidence": "LOW", "description": f"Possible {self.event.data['type']} parameter with {hash_instance.name.upper()} Hash as value. {self.metadata()}, linked to additional parameter [{additional_param_name}]", "context": context, } diff --git a/bbot/modules/lightfuzz/submodules/esi.py b/bbot/modules/lightfuzz/submodules/esi.py index 757a903979..cfb786187c 100644 --- a/bbot/modules/lightfuzz/submodules/esi.py +++ b/bbot/modules/lightfuzz/submodules/esi.py @@ -22,6 +22,9 @@ async def check_probe(self, cookies, probe, match): self.results.append( { "type": "FINDING", + "name": "Edge Side Include Processing", + "severity": "MEDIUM", + "confidence": "HIGH", "description": f"Edge Side Include. Parameter: [{self.event.data['name']}] Parameter Type: [{self.event.data['type']}]{self.conversion_note()}", } ) diff --git a/bbot/modules/lightfuzz/submodules/path.py b/bbot/modules/lightfuzz/submodules/path.py index 44047e2907..eb866eac8f 100644 --- a/bbot/modules/lightfuzz/submodules/path.py +++ b/bbot/modules/lightfuzz/submodules/path.py @@ -121,7 +121,9 @@ async def fuzz(self): if confirmations > 3: self.results.append( { - "type": "FINDING", + "name": "Possible Path Traversal", + "severity": "HIGH", + "confidence": "LOW", "description": f"POSSIBLE Path Traversal. {self.metadata()} Detection Method: [{path_technique}]", } ) @@ -148,7 +150,9 @@ async def fuzz(self): if r and trigger in r.text: self.results.append( { - "type": "FINDING", + "name": "Possible Path Traversal", + "severity": "HIGH", + "confidence": "MEDIUM", "description": f"POSSIBLE Path Traversal. {self.metadata()} Detection Method: [Absolute Path: {path}]", } ) diff --git a/bbot/modules/lightfuzz/submodules/serial.py b/bbot/modules/lightfuzz/submodules/serial.py index 9a7dd90135..508fe98ab9 100644 --- a/bbot/modules/lightfuzz/submodules/serial.py +++ b/bbot/modules/lightfuzz/submodules/serial.py @@ -78,6 +78,18 @@ def is_possibly_serialized(self, value): return True return False + @staticmethod + def payload_language(payload_name): + """Extract the language family from a payload name (e.g. 'java_base64_string_error' -> 'java').""" + return payload_name.split("_")[0] + + async def confirm_baseline(self, control_payload, cookies): + """Re-send the control payload to confirm the baseline error state is stable (not transient).""" + confirmation = await self.standard_probe(self.event.data["type"], cookies, control_payload) + if confirmation is None: + return None + return getattr(confirmation, "status_code", None) + async def fuzz(self): cookies = self.event.data.get("assigned_cookies", {}) control_payload_hex = self.CONTROL_PAYLOAD_HEX @@ -111,13 +123,16 @@ async def fuzz(self): self.debug(f"HttpCompareError encountered: {e}") return + # Map each payload set to its control payload for baseline confirmation + payload_sets = [ + (base64_serialization_payloads, http_compare_base64, control_payload_base64), + (hex_serialization_payloads, http_compare_hex, control_payload_hex), + (php_raw_serialization_payloads, http_compare_php_raw, control_payload_php_raw), + ] + # Proceed with payload probes - for payload_set, payload_baseline in [ - (base64_serialization_payloads, http_compare_base64), - (hex_serialization_payloads, http_compare_hex), - (php_raw_serialization_payloads, http_compare_php_raw), - ]: - for type, payload in payload_set.items(): + for payload_set, payload_baseline, control_payload in payload_sets: + for payload_type, payload in payload_set.items(): try: matches_baseline, diff_reasons, reflection, response = await self.compare_probe( payload_baseline, self.event.data["type"], payload, cookies @@ -127,17 +142,17 @@ async def fuzz(self): continue if matches_baseline: - self.debug(f"Payload {type} matches baseline, skipping") + self.debug(f"Payload {payload_type} matches baseline, skipping") continue - self.debug(f"Probe result for {type}: {response}") + self.debug(f"Probe result for {payload_type}: {response}") status_code = getattr(response, "status_code", 0) if status_code == 0: continue if diff_reasons == ["header"]: - self.debug(f"Only header diffs found for {type}, skipping") + self.debug(f"Only header diffs found for {payload_type}, skipping") continue if status_code not in (200, 500): @@ -145,7 +160,7 @@ async def fuzz(self): continue # if the status code changed to 200, and the response doesn't match our general error exclusions, we have a finding - self.debug(f"Potential finding detected for {type}, needs confirmation") + self.debug(f"Potential finding detected for {payload_type}, needs confirmation") if ( status_code == 200 and "code" in diff_reasons @@ -153,6 +168,14 @@ async def fuzz(self): error in response.text for error in general_errors ) # ensure the 200 is not actually an error ): + # Confirm the baseline error state is stable by re-sending the control payload. + # If the control also returns 200 now, the original error was transient. + confirmation_status = await self.confirm_baseline(control_payload, cookies) + if confirmation_status == 200: + self.debug( + f"Baseline confirmation returned 200 for {payload_type}, original error was transient, skipping" + ) + continue def get_title(text): soup = self.lightfuzz.helpers.beautifulsoup(text, "html.parser") @@ -165,25 +188,47 @@ def get_title(text): self.results.append( { - "type": "FINDING", - "description": f"POSSIBLE Unsafe Deserialization. {self.metadata()} Technique: [Error Resolution (Baseline: [{payload_baseline.baseline.status_code}] {baseline_title} -> Probe: [{status_code}] {probe_title})] Serialization Payload: [{type}]", + "name": "Possible Unsafe Deserialization", + "severity": "HIGH", + "confidence": "LOW", + "description": f"POSSIBLE Unsafe Deserialization. {self.metadata()} Technique: [Error Resolution (Baseline: [{payload_baseline.baseline.status_code}] {baseline_title} -> Probe: [{status_code}] {probe_title})] Serialization Payload: [{payload_type}]", + "_technique": "error_resolution", + "_language": self.payload_language(payload_type), } ) # if the first case doesn't match, we check for a telltale error string like "java.io.optionaldataexception" in the response. # but only if the response is a 500, or a 200 with a body diff elif status_code == 500 or (status_code == 200 and diff_reasons == ["body"]): - self.debug(f"500 status code or body match for {type}") + self.debug(f"500 status code or body match for {payload_type}") for serialization_error in serialization_errors: # check for the error string, but also ensure the error string isn't just always present in the response if ( serialization_error in response.text.lower() and serialization_error not in payload_baseline.baseline.text.lower() ): - self.debug(f"Error string '{serialization_error}' found in response for {type}") + self.debug(f"Error string '{serialization_error}' found in response for {payload_type}") self.results.append( { - "type": "FINDING", - "description": f"POSSIBLE Unsafe Deserialization. {self.metadata()} Technique: [Differential Error Analysis] Error-String: [{serialization_error}] Payload: [{type}]", + "name": "Possible Unsafe Deserialization", + "severity": "HIGH", + "confidence": "LOW", + "description": f"POSSIBLE Unsafe Deserialization. {self.metadata()} Technique: [Differential Error Analysis] Error-String: [{serialization_error}] Payload: [{payload_type}]", } ) break + + # Final safety net: if Error Resolution findings span multiple language families, discard them. + # A real deserialization vuln only deserializes one language's format. + error_resolution_results = [r for r in self.results if r.get("_technique") == "error_resolution"] + if error_resolution_results: + languages = set(r["_language"] for r in error_resolution_results) + if len(languages) > 1: + self.debug( + f"Error Resolution findings span multiple language families ({languages}), discarding as false positives" + ) + self.results = [r for r in self.results if r.get("_technique") != "error_resolution"] + + # Clean up internal metadata keys before results are emitted + for r in self.results: + r.pop("_technique", None) + r.pop("_language", None) diff --git a/bbot/modules/lightfuzz/submodules/sqli.py b/bbot/modules/lightfuzz/submodules/sqli.py index a2adfd2222..4d2633f34f 100644 --- a/bbot/modules/lightfuzz/submodules/sqli.py +++ b/bbot/modules/lightfuzz/submodules/sqli.py @@ -99,7 +99,9 @@ async def fuzz(self): if sqli_error_string.lower() in single_quote[3].text.lower(): self.results.append( { - "type": "FINDING", + "name": "Possible SQL Injection", + "severity": "HIGH", + "confidence": "MEDIUM", "description": f"Possible SQL Injection. {self.metadata()} Detection Method: [SQL Error Detection] Detected String: [{sqli_error_string}]", } ) @@ -119,7 +121,9 @@ async def fuzz(self): ): self.results.append( { - "type": "FINDING", + "name": "Possible SQL Injection", + "severity": "HIGH", + "confidence": "MEDIUM", "description": f"Possible SQL Injection. {self.metadata()} Detection Method: [Single Quote/Two Single Quote, Code Change ({http_compare.baseline.status_code}->{single_quote[3].status_code}->{double_single_quote[3].status_code})]", } ) @@ -171,7 +175,7 @@ async def fuzz(self): ): # decide if the delay is within the detection threshold and constitutes a successful sleep execution confirmations += 1 self.debug( - f"{self.event.data['url']}:{self.event.data['name']}:{self.event.data['type']} Increasing confirmations, now: {str(confirmations)} " + f"{self.event.url}:{self.event.data['name']}:{self.event.data['type']} Increasing confirmations, now: {str(confirmations)} " ) else: break @@ -179,7 +183,9 @@ async def fuzz(self): if confirmations == 3: self.results.append( { - "type": "FINDING", + "name": "Possible Blind SQL Injection", + "severity": "HIGH", + "confidence": "LOW", "description": f"Possible Blind SQL Injection. {self.metadata()} Detection Method: [Delay Probe ({p})]", } ) diff --git a/bbot/modules/lightfuzz/submodules/ssrf.py b/bbot/modules/lightfuzz/submodules/ssrf.py new file mode 100644 index 0000000000..e2c927e489 --- /dev/null +++ b/bbot/modules/lightfuzz/submodules/ssrf.py @@ -0,0 +1,47 @@ +from .base import BaseLightfuzz + + +class ssrf(BaseLightfuzz): + """ + Detects Server-Side Request Forgery (SSRF) vulnerabilities. + + Techniques: + + * OOB (Out-of-Band) Detection: + - Injects URLs pointing to an Interactsh server as parameter values + - Tries multiple URL schemes (http://, https://) + - Detects SSRF through DNS/HTTP interaction callbacks via Interactsh + """ + + friendly_name = "Server-Side Request Forgery" + uses_interactsh = True + + async def fuzz(self): + if not self.lightfuzz.interactsh_instance: + return + + cookies = self.event.data.get("assigned_cookies", {}) + # Try with explicit schemes and bare domain (for cases where the server prepends the scheme) + prefixes = ["http://", "https://", ""] + + for prefix in prefixes: + subdomain_tag = self.lightfuzz.helpers.rand_string(4, digits=False) + interactsh_url = f"{prefix}{subdomain_tag}.{self.lightfuzz.interactsh_domain}" + probe_label = prefix if prefix else "no scheme" + + self.lightfuzz.interactsh_subdomain_tags[subdomain_tag] = { + "event": self.event, + "name": "Server-Side Request Forgery", + "description": f"Server-Side Request Forgery (OOB Interaction) Type: [{self.event.data['type']}] Parameter Name: [{self.event.data['name']}] Probe: [{probe_label}]", + "severity": "HIGH", + "confidence": "CONFIRMED", + "severity_dns": "HIGH", + "confidence_dns": "MEDIUM", + } + + await self.standard_probe( + self.event.data["type"], + cookies, + interactsh_url, + timeout=15, + ) diff --git a/bbot/modules/lightfuzz/submodules/ssti.py b/bbot/modules/lightfuzz/submodules/ssti.py index 544b10b103..187c5ca4bf 100644 --- a/bbot/modules/lightfuzz/submodules/ssti.py +++ b/bbot/modules/lightfuzz/submodules/ssti.py @@ -32,7 +32,9 @@ async def fuzz(self): if r and ("1787569" in r.text or "1,787,569" in r.text): self.results.append( { - "type": "FINDING", + "name": "Possible Server-side Template Injection", + "severity": "HIGH", + "confidence": "HIGH", "description": f"POSSIBLE Server-side Template Injection. {self.metadata()} Detection Method: [Integer Multiplication] Payload: [{probe_value}]", } ) diff --git a/bbot/modules/lightfuzz/submodules/xss.py b/bbot/modules/lightfuzz/submodules/xss.py index 4403e175ca..827afc33e5 100644 --- a/bbot/modules/lightfuzz/submodules/xss.py +++ b/bbot/modules/lightfuzz/submodules/xss.py @@ -90,6 +90,9 @@ async def check_probe(self, cookies, probe, match, context): if probe_result and match in probe_result.text: self.results.append( { + "name": "Possible Reflected XSS", + "severity": "MEDIUM", + "confidence": "MEDIUM", "type": "FINDING", "description": f"Possible Reflected XSS. Parameter: [{self.event.data['name']}] Context: [{context}] Parameter Type: [{self.event.data['type']}]{self.conversion_note()}", } diff --git a/bbot/modules/medusa.py b/bbot/modules/medusa.py index e8814b5c2b..a3f8cebd85 100644 --- a/bbot/modules/medusa.py +++ b/bbot/modules/medusa.py @@ -4,8 +4,8 @@ class medusa(BaseModule): watched_events = ["PROTOCOL"] - produced_events = ["VULNERABILITY"] - flags = ["active", "aggressive", "deadly"] + produced_events = ["FINDING"] + flags = ["active", "loud", "invasive"] per_host_only = True meta = { "description": "Medusa SNMP bruteforcing with v1, v2c and R/W check.", @@ -140,8 +140,18 @@ async def handle_event(self, event): self.info(f"Medusa stderr: {result.stderr}") async for message in self.parse_output(result.stdout, snmp_version): - vuln_event = self.create_vuln_event("CRITICAL", message, event) - await self.emit_event(vuln_event) + await self.emit_event( + { + "name": f"Valid SNMPV{snmp_version} Credentials Found!", + "severity": "CRITICAL", + "confidence": "CONFIRMED", + "host": str(event.host), + "port": str(event.port), + "description": message, + }, + "FINDING", + parent=event, + ) # else: Medusa supports various protocols which could in theory be implemented later on. @@ -215,18 +225,3 @@ async def construct_command(self, host, port, protocol, protocol_version): ] return cmd - - def create_vuln_event(self, severity, description, source_event): - host = str(source_event.host) - port = str(source_event.port) - - return self.make_event( - { - "severity": severity, - "host": host, - "port": port, - "description": description, - }, - "VULNERABILITY", - source_event, - ) diff --git a/bbot/modules/myssl.py b/bbot/modules/myssl.py index 1a04364bcf..82aabc6b99 100644 --- a/bbot/modules/myssl.py +++ b/bbot/modules/myssl.py @@ -2,7 +2,7 @@ class myssl(subdomain_enum): - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { diff --git a/bbot/modules/newsletters.py b/bbot/modules/newsletters.py index 114f7d66fd..e6d8626fd4 100644 --- a/bbot/modules/newsletters.py +++ b/bbot/modules/newsletters.py @@ -18,7 +18,7 @@ class newsletters(BaseModule): watched_events = ["HTTP_RESPONSE"] produced_events = ["FINDING"] - flags = ["active", "safe"] + flags = ["safe", "active"] meta = { "description": "Searches for Newsletter Submission Entry Fields on Websites", "created_date": "2024-02-02", @@ -51,7 +51,14 @@ async def handle_event(self, event): result = self.find_type(soup) if result: description = "Found a Newsletter Submission Form that could be used for email bombing attacks" - data = {"host": str(_event.host), "description": description, "url": _event.data["url"]} + data = { + "host": str(_event.host), + "description": description, + "url": _event.url, + "name": "Newsletter Submission Form", + "severity": "INFO", + "confidence": "LOW", + } await self.emit_event( data, "FINDING", diff --git a/bbot/modules/ntlm.py b/bbot/modules/ntlm.py index 67268616de..164f26efd5 100644 --- a/bbot/modules/ntlm.py +++ b/bbot/modules/ntlm.py @@ -68,7 +68,7 @@ class ntlm(BaseModule): watched_events = ["URL", "HTTP_RESPONSE"] produced_events = ["FINDING", "DNS_NAME"] - flags = ["active", "safe", "web-basic"] + flags = ["safe", "active", "web"] meta = { "description": "Watch for HTTP endpoints that support NTLM authentication", "created_date": "2022-07-25", @@ -86,10 +86,7 @@ async def setup(self): async def handle_event(self, event): found_hash = hash(f"{event.host}:{event.port}") - if event.type == "URL": - url = event.data - else: - url = event.data["url"] + url = event.url if found_hash in self.found: return @@ -120,6 +117,9 @@ async def handle_event(self, event): "host": str(event.host), "url": url, "description": f"NTLM AUTH: {ntlm_resp_decoded}", + "name": "NTLM Authentication", + "severity": "INFO", + "confidence": "HIGH", }, "FINDING", parent=event, diff --git a/bbot/modules/nuclei.py b/bbot/modules/nuclei.py index 1acfa3c683..6602d03e51 100644 --- a/bbot/modules/nuclei.py +++ b/bbot/modules/nuclei.py @@ -6,8 +6,8 @@ class nuclei(BaseModule): watched_events = ["URL"] - produced_events = ["FINDING", "VULNERABILITY", "TECHNOLOGY"] - flags = ["active", "aggressive", "deadly"] + produced_events = ["FINDING", "TECHNOLOGY"] + flags = ["active", "loud", "invasive"] meta = { "description": "Fast and customisable vulnerability scanner", "created_date": "2022-03-12", @@ -143,8 +143,8 @@ async def setup(self): async def handle_batch(self, *events): temp_target = self.helpers.make_target() for e in events: - temp_target.add(e.data, e) - nuclei_input = [str(e.data) for e in events] + temp_target.add(e.url, e) + nuclei_input = [e.url for e in events] async for severity, template, tags, host, url, name, extracted_results in self.execute_nuclei(nuclei_input): # this is necessary because sometimes nuclei is inconsistent about the data returned in the host field cleaned_host = temp_target.get(host) @@ -154,7 +154,7 @@ async def handle_batch(self, *events): continue if url == "": - url = str(parent_event.data) + url = parent_event.url if severity == "INFO" and "tech" in tags: await self.emit_event( @@ -175,6 +175,9 @@ async def handle_batch(self, *events): "host": str(parent_event.host), "url": url, "description": description_string, + "name": f"Nuclei Vuln - {name}", + "severity": "INFO", + "confidence": "HIGH", }, "FINDING", parent_event, @@ -187,8 +190,10 @@ async def handle_batch(self, *events): "host": str(parent_event.host), "url": url, "description": description_string, + "name": f"Nuclei Vuln - {name}", + "confidence": "HIGH", }, - "VULNERABILITY", + "FINDING", parent_event, context=f"{{module}} scanned {url} and identified {severity.lower()} {{event.type}}: {description_string}", ) @@ -199,7 +204,7 @@ def correlate_event(self, events, host): return event self.verbose(f"Failed to correlate nuclei result for {host}. Possible parent events:") for event in events: - self.verbose(f" - {event.data}") + self.verbose(f" - {event.url}") async def execute_nuclei(self, nuclei_input): command = [ @@ -311,9 +316,7 @@ async def cleanup(self): async def filter_event(self, event): if self.config.get("directory_only", True): if "endpoint" in event.tags: - self.debug( - f"rejecting URL [{str(event.data)}] because directory_only is true and event has endpoint tag" - ) + self.debug(f"rejecting URL [{event.url}] because directory_only is true and event has endpoint tag") return False return True diff --git a/bbot/modules/oauth.py b/bbot/modules/oauth.py index 58c0507c09..7bcbc972fa 100644 --- a/bbot/modules/oauth.py +++ b/bbot/modules/oauth.py @@ -6,7 +6,7 @@ class OAUTH(BaseModule): watched_events = ["DNS_NAME", "URL_UNVERIFIED"] produced_events = ["DNS_NAME"] - flags = ["affiliates", "subdomain-enum", "cloud-enum", "web-basic", "active", "safe"] + flags = ["safe", "affiliates", "subdomain-enum", "cloud-enum", "web", "active"] meta = { "description": "Enumerate OAUTH and OpenID Connect services", "created_date": "2023-07-12", @@ -26,15 +26,29 @@ async def setup(self): return True async def filter_event(self, event): - if event.module == self or any(t in event.tags for t in ("target", "domain", "ms-auth-url")): + if event.module == self or any(t in event.tags for t in ("seed", "domain", "ms-auth-url")): return True elif self.try_all and event.scope_distance == 0: return True return False + def _get_source_domain(self, event): + """Walk the parent chain to find the nearest in-scope domain.""" + seen = set() + current = event + while current is not None and id(current) not in seen: + seen.add(id(current)) + if current.host: + _, domain = self.helpers.split_domain(str(current.host)) + if self.scan.in_scope(domain): + return domain + current = current.parent + _, domain = self.helpers.split_domain(str(event.host)) + return domain + async def handle_event(self, event): - _, domain = self.helpers.split_domain(event.data) - source_domain = getattr(event, "source_domain", domain) + _, domain = self.helpers.split_domain(str(event.host)) + source_domain = self._get_source_domain(event) if not self.scan.in_scope(source_domain): return @@ -46,7 +60,7 @@ async def handle_event(self, event): oidc_tasks.append(self.helpers.create_task(self.getoidc(f"https://login.windows.net/{domain}"))) if event.type == "URL_UNVERIFIED": - url = event.data + url = event.url else: url = f"https://{event.data}" @@ -62,15 +76,17 @@ async def handle_event(self, event): if token_endpoint: finding_event = self.make_event( { + "name": "OpenID Connect Endpoint", "description": f"OpenID Connect Endpoint (domain: {source_domain}) found at {url}", "host": event.host, "url": url, + "severity": "INFO", + "confidence": "HIGH", }, "FINDING", parent=event, ) if finding_event: - finding_event.source_domain = source_domain await self.emit_event( finding_event, context=f'{{module}} identified {{event.type}}: OpenID Connect Endpoint for "{source_domain}" at {url}', @@ -79,20 +95,19 @@ async def handle_event(self, event): token_endpoint, "URL_UNVERIFIED", parent=event, tags=["affiliate", "oauth-token-endpoint"] ) if url_event: - url_event.source_domain = source_domain await self.emit_event( url_event, context=f'{{module}} identified OpenID Connect Endpoint for "{source_domain}" at {{event.type}}: {url}', ) for result in oidc_results: - if result not in (domain, event.data): + if result not in (domain, str(event.host)): event_type = "URL_UNVERIFIED" if self.helpers.is_url(result) else "DNS_NAME" await self.emit_event( result, event_type, parent=event, tags=["affiliate"], - context=f'{{module}} analyzed OpenID configuration for "{source_domain}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} analyzed OpenID configuration for "{source_domain}" and found {{event.type}}: {{event.pretty_string}}', ) for oauth_task in oauth_tasks: @@ -101,15 +116,17 @@ async def handle_event(self, event): description = f"Potentially Sprayable OAUTH Endpoint (domain: {source_domain}) at {url}" oauth_finding = self.make_event( { + "name": "Potentially Sprayable OAUTH Endpoint", "description": description, "host": event.host, "url": url, + "severity": "INFO", + "confidence": "LOW", }, "FINDING", parent=event, ) if oauth_finding: - oauth_finding.source_domain = source_domain await self.emit_event( oauth_finding, context=f"{{module}} identified {{event.type}}: {description}", diff --git a/bbot/modules/otx.py b/bbot/modules/otx.py index e20ad1a655..a22a5636b5 100644 --- a/bbot/modules/otx.py +++ b/bbot/modules/otx.py @@ -2,7 +2,7 @@ class otx(subdomain_enum_apikey): - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { diff --git a/bbot/modules/output/asset_inventory.py b/bbot/modules/output/asset_inventory.py index 49c26fa8d7..7901d6ac8b 100644 --- a/bbot/modules/output/asset_inventory.py +++ b/bbot/modules/output/asset_inventory.py @@ -26,7 +26,6 @@ class asset_inventory(CSV): "DNS_NAME", "URL", "FINDING", - "VULNERABILITY", "TECHNOLOGY", "IP_ADDRESS", "WAF", @@ -186,7 +185,8 @@ async def finish(self): asset.host, "DNS_NAME", parent=self.scan.root_event, raise_error=True ) await self.emit_event( - host_event, context="{module} emitted previous result: {event.type}: {event.data}" + host_event, + context="{module} emitted previous result: {event.type}: {event.pretty_string}", ) for port in asset.ports: netloc = self.helpers.make_netloc(asset.host, port) @@ -194,7 +194,7 @@ async def finish(self): if open_port_event: await self.emit_event( open_port_event, - context="{module} emitted previous result: {event.type}: {event.data}", + context="{module} emitted previous result: {event.type}: {event.pretty_string}", ) else: for ip in asset.ip_addresses: @@ -202,7 +202,8 @@ async def finish(self): ip, "IP_ADDRESS", parent=self.scan.root_event, raise_error=True ) await self.emit_event( - ip_event, context="{module} emitted previous result: {event.type}: {event.data}" + ip_event, + context="{module} emitted previous result: {event.type}: {event.pretty_string}", ) for port in asset.ports: netloc = self.helpers.make_netloc(ip, port) @@ -210,7 +211,7 @@ async def finish(self): if open_port_event: await self.emit_event( open_port_event, - context="{module} emitted previous result: {event.type}: {event.data}", + context="{module} emitted previous result: {event.type}: {event.pretty_string}", ) else: self.warning( @@ -314,14 +315,11 @@ def absorb_event(self, event): self.ports.add(str(event.port)) if event.type == "FINDING": - location = event.data.get("url", event.data.get("host", "")) + location = event.url or event.data.get("host", "") if location: - self.findings.add(f"{location}:{event.data['description']}") - - if event.type == "VULNERABILITY": - location = event.data.get("url", event.data.get("host", "")) - if location: - self.findings.add(f"{location}:{event.data['description']}:{event.data['severity']}") + self.findings.add( + f"{location}:{event.data['description']}:Severity: {event.data['severity']} Confidence: {event.data['confidence']}" + ) severity_int = severity_map.get(event.data.get("severity", "N/A"), 0) if severity_int > self.risk_rating: self.risk_rating = severity_int @@ -339,9 +337,10 @@ def absorb_event(self, event): if update_http_status or not self.http_title: self.http_title = title - for tag in event.tags: - if tag.startswith("cdn-") or tag.startswith("cloud-"): - self.provider = tag + for host_meta in event.host_metadata.values(): + providers = host_meta.get("cloud_providers", {}) + if providers: + self.provider = ", ".join(providers.keys()) break @property diff --git a/bbot/modules/output/base.py b/bbot/modules/output/base.py index 5aa17d24c1..fefb4f3fad 100644 --- a/bbot/modules/output/base.py +++ b/bbot/modules/output/base.py @@ -46,6 +46,9 @@ def _event_precheck(self, event): else: return False, "its type is omitted in the config" + if event.always_emit: + return True, "event is always emitted" + # internal events like those from speculate, ipneighbor # or events that are over our report distance if event._internal: diff --git a/bbot/modules/output/discord.py b/bbot/modules/output/discord.py index 2aa4d21f84..934d89e670 100644 --- a/bbot/modules/output/discord.py +++ b/bbot/modules/output/discord.py @@ -8,10 +8,10 @@ class Discord(WebhookOutputModule): "created_date": "2023-08-14", "author": "@TheTechromancer", } - options = {"webhook_url": "", "event_types": ["VULNERABILITY", "FINDING"], "min_severity": "LOW", "retries": 10} + options = {"webhook_url": "", "event_types": ["FINDING"], "min_severity": "LOW", "retries": 10} options_desc = { "webhook_url": "Discord webhook URL", "event_types": "Types of events to send", - "min_severity": "Only allow VULNERABILITY events of this severity or higher", + "min_severity": "Only allow FINDING events of this severity or higher", "retries": "Number of times to retry sending the message before skipping the event", } diff --git a/bbot/modules/output/elastic.py b/bbot/modules/output/elastic.py new file mode 100644 index 0000000000..064c00af7c --- /dev/null +++ b/bbot/modules/output/elastic.py @@ -0,0 +1,32 @@ +from .http import HTTP + + +class Elastic(HTTP): + """ + docker run -d -p 9200:9200 --name=bbot-elastic --v "$(pwd)/elastic_data:/usr/share/elasticsearch/data" -e ELASTIC_PASSWORD=bbotislife -m 1GB docker.elastic.co/elasticsearch/elasticsearch:8.16.0 + """ + + watched_events = ["*"] + meta = { + "description": "Send scan results to Elasticsearch", + "created_date": "2022-11-21", + "author": "@TheTechromancer", + } + options = { + "url": "https://localhost:9200/bbot_events/_doc", + "username": "elastic", + "password": "bbotislife", + "timeout": 10, + } + options_desc = { + "url": "Elastic URL (e.g. https://localhost:9200//_doc)", + "username": "Elastic username", + "password": "Elastic password", + "timeout": "HTTP timeout", + } + + async def cleanup(self): + # refresh the index + doc_regex = self.helpers.re.compile(r"/[^/]+$") + refresh_url = doc_regex.sub("/_refresh", self.url) + await self.helpers.request(refresh_url, auth=self.auth) diff --git a/bbot/modules/output/emails.py b/bbot/modules/output/emails.py index 60d9a153c5..6bfe940689 100644 --- a/bbot/modules/output/emails.py +++ b/bbot/modules/output/emails.py @@ -4,7 +4,7 @@ class Emails(TXT): watched_events = ["EMAIL_ADDRESS"] - flags = ["email-enum"] + flags = ["safe", "email-enum"] meta = { "description": "Output any email addresses found belonging to the target domain", "created_date": "2023-12-23", @@ -27,7 +27,7 @@ def _scope_distance_check(self, event): async def handle_event(self, event): if self.file is not None: self.emails_written += 1 - self.file.write(f"{event.data}\n") + self.file.write(f"{event.pretty_string}\n") self.file.flush() async def report(self): diff --git a/bbot/modules/output/http.py b/bbot/modules/output/http.py index 9d9241da0b..28fa917fc7 100644 --- a/bbot/modules/output/http.py +++ b/bbot/modules/output/http.py @@ -1,3 +1,6 @@ +from omegaconf import OmegaConf + +from bbot.models.pydantic import Event from bbot.modules.output.base import BaseOutputModule @@ -14,8 +17,8 @@ class HTTP(BaseOutputModule): "bearer": "", "username": "", "password": "", + "headers": {}, "timeout": 10, - "siem_friendly": False, } options_desc = { "url": "Web URL", @@ -23,16 +26,15 @@ class HTTP(BaseOutputModule): "bearer": "Authorization Bearer token", "username": "Username (basic auth)", "password": "Password (basic auth)", + "headers": "Additional headers to send with the request", "timeout": "HTTP timeout", - "siem_friendly": "Format JSON in a SIEM-friendly way for ingestion into Elastic, Splunk, etc.", } async def setup(self): self.url = self.config.get("url", "") self.method = self.config.get("method", "POST") self.timeout = self.config.get("timeout", 10) - self.siem_friendly = self.config.get("siem_friendly", False) - self.headers = {} + self.headers = OmegaConf.to_object(self.config.get("headers", OmegaConf.create())) bearer = self.config.get("bearer", "") if bearer: self.headers["Authorization"] = f"Bearer {bearer}" @@ -51,12 +53,15 @@ async def setup(self): async def handle_event(self, event): while 1: + event_json = event.json() + event_pydantic = Event(**event_json) + event_json = event_pydantic.model_dump(exclude_none=True) response = await self.helpers.request( url=self.url, method=self.method, auth=self.auth, headers=self.headers, - json=event.json(siem_friendly=self.siem_friendly), + json=event_json, ) is_success = False if response is None else response.is_success if not is_success: diff --git a/bbot/modules/output/json.py b/bbot/modules/output/json.py index a35fa6aed7..b93d1e4e3f 100644 --- a/bbot/modules/output/json.py +++ b/bbot/modules/output/json.py @@ -11,20 +11,18 @@ class JSON(BaseOutputModule): "created_date": "2022-04-07", "author": "@TheTechromancer", } - options = {"output_file": "", "siem_friendly": False} + options = {"output_file": ""} options_desc = { "output_file": "Output to file", - "siem_friendly": "Output JSON in a SIEM-friendly format for ingestion into Elastic, Splunk, etc.", } _preserve_graph = True async def setup(self): self._prep_output_dir("output.json") - self.siem_friendly = self.config.get("siem_friendly", False) return True async def handle_event(self, event): - event_json = event.json(siem_friendly=self.siem_friendly) + event_json = event.json() event_str = json.dumps(event_json) if self.file is not None: self.file.write(event_str + "\n") diff --git a/bbot/modules/output/kafka.py b/bbot/modules/output/kafka.py new file mode 100644 index 0000000000..01eeeb2fd6 --- /dev/null +++ b/bbot/modules/output/kafka.py @@ -0,0 +1,48 @@ +import json +from aiokafka import AIOKafkaProducer + +from bbot.modules.output.base import BaseOutputModule + + +class Kafka(BaseOutputModule): + watched_events = ["*"] + meta = { + "description": "Output scan data to a Kafka topic", + "created_date": "2024-11-22", + "author": "@TheTechromancer", + } + options = { + "bootstrap_servers": "localhost:9092", + "topic": "bbot_events", + } + options_desc = { + "bootstrap_servers": "A comma-separated list of Kafka server addresses", + "topic": "The Kafka topic to publish events to", + } + deps_pip = ["aiokafka~=0.12.0"] + + async def setup(self): + self.bootstrap_servers = self.config.get("bootstrap_servers", "localhost:9092") + self.topic = self.config.get("topic", "bbot_events") + self.producer = AIOKafkaProducer(bootstrap_servers=self.bootstrap_servers) + + # Start the producer + await self.producer.start() + self.verbose("Kafka producer started successfully") + return True + + async def handle_event(self, event): + event_json = event.json() + event_data = json.dumps(event_json).encode("utf-8") + while 1: + try: + await self.producer.send_and_wait(self.topic, event_data) + break + except Exception as e: + self.warning(f"Error sending event to Kafka: {e}, retrying...") + await self.helpers.sleep(1) + + async def cleanup(self): + # Stop the producer + await self.producer.stop() + self.verbose("Kafka producer stopped successfully") diff --git a/bbot/modules/output/mongo.py b/bbot/modules/output/mongo.py new file mode 100644 index 0000000000..833eab2304 --- /dev/null +++ b/bbot/modules/output/mongo.py @@ -0,0 +1,98 @@ +from contextlib import suppress + +from pymongo import AsyncMongoClient + +from bbot.models.pydantic import Event, Scan, Target +from bbot.modules.output.base import BaseOutputModule + + +class Mongo(BaseOutputModule): + """ + docker run --rm -p 27017:27017 mongo + """ + + watched_events = ["*"] + meta = { + "description": "Output scan data to a MongoDB database", + "created_date": "2024-11-17", + "author": "@TheTechromancer", + } + options = { + "uri": "mongodb://localhost:27017", + "database": "bbot", + "username": "", + "password": "", + "collection_prefix": "", + } + options_desc = { + "uri": "The URI of the MongoDB server", + "database": "The name of the database to use", + "username": "The username to use to connect to the database", + "password": "The password to use to connect to the database", + "collection_prefix": "Prefix the name of each collection with this string", + } + deps_pip = ["pymongo~=4.15"] + + async def setup(self): + self.uri = self.config.get("uri", "mongodb://localhost:27017") + self.username = self.config.get("username", "") + self.password = self.config.get("password", "") + self.db_client = AsyncMongoClient(self.uri, username=self.username, password=self.password) + + # Ping the server to confirm a successful connection + try: + await self.db_client.admin.command("ping") + self.verbose("MongoDB connection successful") + except Exception as e: + return False, f"Failed to connect to MongoDB: {e}" + + self.db_name = self.config.get("database", "bbot") + self.db = self.db_client[self.db_name] + self.collection_prefix = self.config.get("collection_prefix", "") + self.events_collection = self.db[f"{self.collection_prefix}events"] + self.scans_collection = self.db[f"{self.collection_prefix}scans"] + self.targets_collection = self.db[f"{self.collection_prefix}targets"] + + # Build an index for each field in reverse_host and host + for fieldname, metadata in Event.indexed_fields().items(): + if "indexed" in metadata: + unique = "unique" in metadata + await self.events_collection.create_index([(fieldname, 1)], unique=unique) + self.verbose(f"Index created for field: {fieldname} (unique={unique})") + + return True + + async def handle_event(self, event): + event_json = event.json() + event_pydantic = Event(**event_json) + while 1: + try: + await self.events_collection.insert_one(event_pydantic.model_dump()) + break + except Exception as e: + self.warning(f"Error inserting event into MongoDB: {e}, retrying...") + self.trace() + await self.helpers.sleep(1) + + if event.type == "SCAN": + scan_json = Scan(**event.data_json).model_dump() + existing_scan = await self.scans_collection.find_one({"id": event_pydantic.id}) + if existing_scan: + await self.scans_collection.replace_one({"id": event_pydantic.id}, scan_json) + self.verbose(f"Updated scan event with ID: {event_pydantic.id}") + else: + # Insert as a new scan if no existing scan is found + await self.scans_collection.insert_one(event_pydantic.model_dump()) + self.verbose(f"Inserted new scan event with ID: {event_pydantic.id}") + + target_data = scan_json.get("target", {}) + target = Target(**target_data) + existing_target = await self.targets_collection.find_one({"hash": target.hash}) + if existing_target: + await self.targets_collection.replace_one({"hash": target.hash}, target.model_dump()) + else: + await self.targets_collection.insert_one(target.model_dump()) + + async def cleanup(self): + with suppress(Exception): + await self.db_client.aclose() diff --git a/bbot/modules/output/nats.py b/bbot/modules/output/nats.py new file mode 100644 index 0000000000..569645cc3b --- /dev/null +++ b/bbot/modules/output/nats.py @@ -0,0 +1,53 @@ +import json +import nats +from bbot.modules.output.base import BaseOutputModule + + +class NATS(BaseOutputModule): + watched_events = ["*"] + meta = { + "description": "Output scan data to a NATS subject", + "created_date": "2024-11-22", + "author": "@TheTechromancer", + } + options = { + "servers": [], + "subject": "bbot_events", + } + options_desc = { + "servers": "A list of NATS server addresses", + "subject": "The NATS subject to publish events to", + } + deps_pip = ["nats-py"] + + async def setup(self): + self.servers = list(self.config.get("servers", [])) + if not self.servers: + return False, "NATS servers are required" + self.subject = self.config.get("subject", "bbot_events") + + # Connect to the NATS server + try: + self.nc = await nats.connect(self.servers) + except Exception as e: + import traceback + + return False, f"Error connecting to NATS: {e}\n{traceback.format_exc()}" + self.verbose("NATS client connected successfully") + return True + + async def handle_event(self, event): + event_json = event.json() + event_data = json.dumps(event_json).encode("utf-8") + while 1: + try: + await self.nc.publish(self.subject, event_data) + break + except Exception as e: + self.warning(f"Error sending event to NATS: {e}, retrying...") + await self.helpers.sleep(1) + + async def cleanup(self): + # Close the NATS connection + await self.nc.close() + self.verbose("NATS client disconnected successfully") diff --git a/bbot/modules/output/neo4j.py b/bbot/modules/output/neo4j.py index b859af516f..8d88572575 100644 --- a/bbot/modules/output/neo4j.py +++ b/bbot/modules/output/neo4j.py @@ -92,7 +92,7 @@ async def handle_batch(self, *all_events): src_id = all_ids[parent.id] dst_id = all_ids[event.id] except KeyError as e: - self.error(f'Error "{e}" correlating {parent.id}:{parent.data} --> {event.id}:{event.data}') + self.error(f'Error "{e}" correlating {parent.id}:{parent.data} --> {event.id}:{event.pretty_string}') continue rel_ids.append((src_id, module, timestamp, dst_id)) diff --git a/bbot/modules/output/nmap_xml.py b/bbot/modules/output/nmap_xml.py index 52698e0de8..9a0cee27eb 100644 --- a/bbot/modules/output/nmap_xml.py +++ b/bbot/modules/output/nmap_xml.py @@ -1,6 +1,7 @@ import sys from xml.dom import minidom from datetime import datetime +from zoneinfo import ZoneInfo from xml.etree.ElementTree import Element, SubElement, tostring from bbot import __version__ @@ -76,7 +77,7 @@ async def handle_event(self, event): async def report(self): scan_start_time = str(int(self.scan.start_time.timestamp())) scan_start_time_str = self.scan.start_time.strftime("%a %b %d %H:%M:%S %Y") - scan_end_time = datetime.now() + scan_end_time = datetime.now(ZoneInfo("UTC")) scan_end_time_str = scan_end_time.strftime("%a %b %d %H:%M:%S %Y") scan_end_time_timestamp = str(scan_end_time.timestamp()) scan_duration = scan_end_time - self.scan.start_time diff --git a/bbot/modules/output/rabbitmq.py b/bbot/modules/output/rabbitmq.py new file mode 100644 index 0000000000..ba4205940d --- /dev/null +++ b/bbot/modules/output/rabbitmq.py @@ -0,0 +1,56 @@ +import json +import aio_pika + +from bbot.modules.output.base import BaseOutputModule + + +class RabbitMQ(BaseOutputModule): + watched_events = ["*"] + meta = { + "description": "Output scan data to a RabbitMQ queue", + "created_date": "2024-11-22", + "author": "@TheTechromancer", + } + options = { + "url": "amqp://guest:guest@localhost/", + "queue": "bbot_events", + } + options_desc = { + "url": "The RabbitMQ connection URL", + "queue": "The RabbitMQ queue to publish events to", + } + deps_pip = ["aio_pika~=9.5.0"] + + async def setup(self): + self.rabbitmq_url = self.config.get("url", "amqp://guest:guest@localhost/") + self.queue_name = self.config.get("queue", "bbot_events") + + # Connect to RabbitMQ + self.connection = await aio_pika.connect_robust(self.rabbitmq_url) + self.channel = await self.connection.channel() + + # Declare the queue + self.queue = await self.channel.declare_queue(self.queue_name, durable=True) + self.verbose("RabbitMQ connection and queue setup successfully") + return True + + async def handle_event(self, event): + event_json = event.json() + event_data = json.dumps(event_json).encode("utf-8") + + # Publish the message to the queue + while 1: + try: + await self.channel.default_exchange.publish( + aio_pika.Message(body=event_data), + routing_key=self.queue_name, + ) + break + except Exception as e: + self.error(f"Error publishing message to RabbitMQ: {e}, rerying...") + await self.helpers.sleep(1) + + async def cleanup(self): + # Close the connection + await self.connection.close() + self.verbose("RabbitMQ connection closed successfully") diff --git a/bbot/modules/output/slack.py b/bbot/modules/output/slack.py index d65c816b3e..299d7334cf 100644 --- a/bbot/modules/output/slack.py +++ b/bbot/modules/output/slack.py @@ -10,23 +10,22 @@ class Slack(WebhookOutputModule): "created_date": "2023-08-14", "author": "@TheTechromancer", } - options = {"webhook_url": "", "event_types": ["VULNERABILITY", "FINDING"], "min_severity": "LOW", "retries": 10} + options = {"webhook_url": "", "event_types": ["FINDING"], "min_severity": "LOW", "retries": 10} options_desc = { "webhook_url": "Discord webhook URL", "event_types": "Types of events to send", - "min_severity": "Only allow VULNERABILITY events of this severity or higher", + "min_severity": "Only allow FINDING events of this severity or higher", "retries": "Number of times to retry sending the message before skipping the event", } content_key = "text" def format_message_str(self, event): event_tags = ",".join(sorted(event.tags)) - return f"`[{event.type}]`\t*`{event.data}`*\t`{event_tags}`" + return f"`[{event.type}]`\t*`{event.pretty_string}`*\t`{event_tags}`" def format_message_other(self, event): event_yaml = yaml.dump(event.data) event_type = f"*`[{event.type}]`*" - if event.type in ("VULNERABILITY", "FINDING"): - event_str, color = self.get_severity_color(event) - event_type = f"{color} `{event_str}` {color}" + event_str, severity_color, confidence_color = self.get_colors(event) + event_type = f"Severity: {severity_color} Confidence: {confidence_color} {event_str}" return f"""*{event_type}*\n```\n{event_yaml}```""" diff --git a/bbot/modules/output/stdout.py b/bbot/modules/output/stdout.py index 59a121bd47..974b7ede16 100644 --- a/bbot/modules/output/stdout.py +++ b/bbot/modules/output/stdout.py @@ -15,7 +15,13 @@ class Stdout(BaseOutputModule): "in_scope_only": "Whether to only show in-scope events", "accept_dupes": "Whether to show duplicate events, default True", } - vuln_severity_map = {"LOW": "HUGEWARNING", "MEDIUM": "HUGEWARNING", "HIGH": "CRITICAL", "CRITICAL": "CRITICAL"} + vuln_severity_map = { + "INFO": "HUGEINFO", + "LOW": "HUGEWARNING", + "MEDIUM": "HUGEWARNING", + "HIGH": "CRITICAL", + "CRITICAL": "CRITICAL", + } format_choices = ["text", "json"] async def setup(self): @@ -40,6 +46,7 @@ async def filter_event(self, event): async def handle_event(self, event): json_mode = "human" if self.text_format == "text" else "json" event_json = event.json(mode=json_mode) + if self.show_event_fields: event_json = {k: str(event_json.get(k, "")) for k in self.show_event_fields} @@ -54,14 +61,14 @@ async def handle_text(self, event, event_json): else: event_str = self.human_event_str(event) - # log vulnerabilities in vivid colors - if event.type == "VULNERABILITY": + # log findings in vivid colors based on severity + if event.type == "FINDING": severity = event.data.get("severity", "INFO") if severity in self.vuln_severity_map: loglevel = self.vuln_severity_map[severity] log_to_stderr(event_str, level=loglevel, logname=False) - elif event.type == "FINDING": - log_to_stderr(event_str, level="HUGEINFO", logname=False) + else: + log_to_stderr(event_str, level="HUGEINFO", logname=False) print(event_str) diff --git a/bbot/modules/output/subdomains.py b/bbot/modules/output/subdomains.py index 6c2bfb0b02..65d082355e 100644 --- a/bbot/modules/output/subdomains.py +++ b/bbot/modules/output/subdomains.py @@ -4,7 +4,7 @@ class Subdomains(TXT): watched_events = ["DNS_NAME", "DNS_NAME_UNRESOLVED"] - flags = ["subdomain-enum"] + flags = ["safe", "subdomain-enum"] meta = { "description": "Output only resolved, in-scope subdomains", "created_date": "2023-07-31", @@ -33,7 +33,7 @@ def _scope_distance_check(self, event): async def handle_event(self, event): if self.file is not None: self.subdomains_written += 1 - self.file.write(f"{event.data}\n") + self.file.write(f"{event.pretty_string}\n") self.file.flush() async def report(self): diff --git a/bbot/modules/output/teams.py b/bbot/modules/output/teams.py index c9a7cf1820..2ab461d5a6 100644 --- a/bbot/modules/output/teams.py +++ b/bbot/modules/output/teams.py @@ -8,11 +8,11 @@ class Teams(WebhookOutputModule): "created_date": "2023-08-14", "author": "@TheTechromancer", } - options = {"webhook_url": "", "event_types": ["VULNERABILITY", "FINDING"], "min_severity": "LOW", "retries": 10} + options = {"webhook_url": "", "event_types": ["FINDING"], "min_severity": "LOW", "retries": 10} options_desc = { "webhook_url": "Teams webhook URL", "event_types": "Types of events to send", - "min_severity": "Only allow VULNERABILITY events of this severity or higher", + "min_severity": "Only allow FINDING events of this severity or higher", "retries": "Number of times to retry sending the message before skipping the event", } @@ -46,7 +46,7 @@ def format_message_other(self, event): def get_severity_color(self, event): color = "Accent" - if event.type == "VULNERABILITY": + if event.type == "FINDING": severity = event.data.get("severity", "INFO") if severity == "CRITICAL": color = "Attention" @@ -78,7 +78,7 @@ def format_message(self, event): heading = {"type": "TextBlock", "text": f"{event.type}", "wrap": True, "size": "Large", "style": "heading"} body = adaptive_card["attachments"][0]["content"]["body"] body.append(heading) - if event.type in ("VULNERABILITY", "FINDING"): + if event.type == "FINDING": subheading = { "type": "TextBlock", "text": event.data.get("severity", "INFO"), diff --git a/bbot/modules/output/web_report.py b/bbot/modules/output/web_report.py index 92ff98289f..4cd2412046 100644 --- a/bbot/modules/output/web_report.py +++ b/bbot/modules/output/web_report.py @@ -4,7 +4,7 @@ class web_report(BaseOutputModule): - watched_events = ["URL", "TECHNOLOGY", "FINDING", "VULNERABILITY", "VHOST"] + watched_events = ["URL", "TECHNOLOGY", "FINDING", "VHOST"] meta = { "description": "Create a markdown report with web assets", "created_date": "2023-02-08", @@ -57,7 +57,7 @@ async def handle_event(self, event): + f" ({event.module})---> " + f"[{event.type}]:{html.escape(event.pretty_string)}" ) - self.web_assets[host]["URL"].append(f"**{html.escape(event.data)}**: {parent_chain_text}") + self.web_assets[host]["URL"].append(f"**{html.escape(event.pretty_string)}**: {parent_chain_text}") else: current_parent = event.parent @@ -89,7 +89,7 @@ async def report(self): if e in dedupe: continue dedupe.append(e) - self.markdown += f"\n* {e}\n" + self.markdown += f"* {e}\n" self.markdown += "\n" if self.file is not None: diff --git a/bbot/modules/output/zeromq.py b/bbot/modules/output/zeromq.py new file mode 100644 index 0000000000..938f234545 --- /dev/null +++ b/bbot/modules/output/zeromq.py @@ -0,0 +1,46 @@ +import zmq +import json + +from bbot.modules.output.base import BaseOutputModule + + +class ZeroMQ(BaseOutputModule): + watched_events = ["*"] + meta = { + "description": "Output scan data to a ZeroMQ socket (PUB)", + "created_date": "2024-11-22", + "author": "@TheTechromancer", + } + options = { + "zmq_address": "", + } + options_desc = { + "zmq_address": "The ZeroMQ socket address to publish events to (e.g. tcp://localhost:5555)", + } + + async def setup(self): + self.zmq_address = self.config.get("zmq_address", "") + if not self.zmq_address: + return False, "ZeroMQ address is required" + self.context = zmq.asyncio.Context() + self.socket = self.context.socket(zmq.PUB) + self.socket.bind(self.zmq_address) + self.verbose("ZeroMQ publisher socket bound successfully") + return True + + async def handle_event(self, event): + event_json = event.json() + event_data = json.dumps(event_json).encode("utf-8") + while 1: + try: + await self.socket.send(event_data) + break + except Exception as e: + self.warning(f"Error sending event to ZeroMQ: {e}, retrying...") + await self.helpers.sleep(1) + + async def cleanup(self): + # Close the socket + self.socket.close() + self.context.term() + self.verbose("ZeroMQ publisher socket closed successfully") diff --git a/bbot/modules/paramminer_cookies.py b/bbot/modules/paramminer_cookies.py index a3b4619d45..871238d803 100644 --- a/bbot/modules/paramminer_cookies.py +++ b/bbot/modules/paramminer_cookies.py @@ -8,8 +8,7 @@ class paramminer_cookies(paramminer_headers): watched_events = ["HTTP_RESPONSE", "WEB_PARAMETER"] produced_events = ["WEB_PARAMETER"] - produced_events = ["FINDING"] - flags = ["active", "aggressive", "slow", "web-paramminer"] + flags = ["active", "loud", "slow", "web-paramminer"] meta = { "description": "Smart brute-force to check for common HTTP cookie parameters", "created_date": "2022-06-27", diff --git a/bbot/modules/paramminer_getparams.py b/bbot/modules/paramminer_getparams.py index e6f35f6235..27a99f8ab4 100644 --- a/bbot/modules/paramminer_getparams.py +++ b/bbot/modules/paramminer_getparams.py @@ -8,8 +8,7 @@ class paramminer_getparams(paramminer_headers): watched_events = ["HTTP_RESPONSE", "WEB_PARAMETER"] produced_events = ["WEB_PARAMETER"] - produced_events = ["FINDING"] - flags = ["active", "aggressive", "slow", "web-paramminer"] + flags = ["active", "loud", "slow", "web-paramminer"] meta = { "description": "Use smart brute-force to check for common HTTP GET parameters", "created_date": "2022-06-28", diff --git a/bbot/modules/paramminer_headers.py b/bbot/modules/paramminer_headers.py index d3cbeb0661..ae573abadf 100644 --- a/bbot/modules/paramminer_headers.py +++ b/bbot/modules/paramminer_headers.py @@ -11,7 +11,7 @@ class paramminer_headers(BaseModule): watched_events = ["HTTP_RESPONSE", "WEB_PARAMETER"] produced_events = ["WEB_PARAMETER"] - flags = ["active", "aggressive", "slow", "web-paramminer"] + flags = ["active", "loud", "slow", "web-paramminer"] meta = { "description": "Use smart brute-force to check for common HTTP header parameters", "created_date": "2022-04-15", @@ -132,7 +132,7 @@ async def do_mining(self, wl, url, batch_size, compare_helper): return results async def process_results(self, event, results): - url = event.data.get("url") + url = event.url for result, reasons, reflection in results: paramtype = self.compare_mode.upper() if paramtype == "HEADER": @@ -173,7 +173,7 @@ async def handle_event(self, event): self.extracted_words_master.add(parameter_name) elif event.type == "HTTP_RESPONSE": - url = event.data.get("url") + url = event.url try: compare_helper = self.helpers.http_compare(url) except HttpCompareError as e: @@ -196,7 +196,7 @@ async def handle_event(self, event): try: results = await self.do_mining(self.wl, url, batch_size, compare_helper) except HttpCompareError as e: - self.debug(f"Encountered HttpCompareError: [{e}] for URL [{event.data}]") + self.debug(f"Encountered HttpCompareError: [{e}] for URL [{event.url}]") await self.process_results(event, results) async def count_test(self, url): @@ -264,7 +264,7 @@ async def finish(self): async def filter_event(self, event): # Filter out static endpoints - if event.data.get("url").endswith(tuple(f".{ext}" for ext in self.config.get("url_extension_static", []))): + if event.url.endswith(tuple(f".{ext}" for ext in self.config.get("url_extension_static", []))): return False # We don't need to look at WEB_PARAMETERS that we produced diff --git a/bbot/modules/passivetotal.py b/bbot/modules/passivetotal.py index b72f43cefd..1010980945 100644 --- a/bbot/modules/passivetotal.py +++ b/bbot/modules/passivetotal.py @@ -4,7 +4,7 @@ class passivetotal(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query the PassiveTotal API for subdomains", "created_date": "2022-08-08", diff --git a/bbot/modules/pgp.py b/bbot/modules/pgp.py index 0c53c2ad42..b12372dd1e 100644 --- a/bbot/modules/pgp.py +++ b/bbot/modules/pgp.py @@ -4,13 +4,12 @@ class pgp(subdomain_enum): watched_events = ["DNS_NAME"] produced_events = ["EMAIL_ADDRESS"] - flags = ["passive", "email-enum", "safe"] + flags = ["safe", "passive", "email-enum"] meta = { "description": "Query common PGP servers for email addresses", "created_date": "2022-08-10", "author": "@TheTechromancer", } - # TODO: scan for Web Key Directory (/.well-known/openpgpkey/) options = { "search_urls": [ "https://keyserver.ubuntu.com/pks/lookup?fingerprint=on&op=vindex&search=", @@ -31,7 +30,7 @@ async def handle_event(self, event): "EMAIL_ADDRESS", event, abort_if=self.abort_if, - context=f'{{module}} queried PGP keyserver {keyserver} for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} queried PGP keyserver {keyserver} for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) async def query(self, query): @@ -40,7 +39,6 @@ async def query(self, query): urls = [url.replace("", self.helpers.quote(query)) for url in urls] async for url, response in self.helpers.request_batch(urls): keyserver = self.helpers.urlparse(url).netloc - response = await self.helpers.request(url) if response is not None: for email in await self.helpers.re.extract_emails(response.text): email = email.lower() diff --git a/bbot/modules/portfilter.py b/bbot/modules/portfilter.py index 43b3e97d5b..21b0313194 100644 --- a/bbot/modules/portfilter.py +++ b/bbot/modules/portfilter.py @@ -3,18 +3,18 @@ class portfilter(BaseInterceptModule): watched_events = ["OPEN_TCP_PORT", "URL_UNVERIFIED", "URL"] - flags = ["passive", "safe"] + flags = ["safe", "passive"] meta = { "description": "Filter out unwanted open ports from cloud/CDN targets", "created_date": "2025-01-06", "author": "@TheTechromancer", } options = { - "cdn_tags": "cdn-", + "cdn_tags": "cdn,waf", "allowed_cdn_ports": "80,443", } options_desc = { - "cdn_tags": "Comma-separated list of tags to skip, e.g. 'cdn,cloud'", + "cdn_tags": "Comma-separated list of tags to skip, e.g. 'cdn,waf'", "allowed_cdn_ports": "Comma-separated list of ports that are allowed to be scanned for CDNs", } @@ -36,10 +36,9 @@ async def handle_event(self, event, **kwargs): # if the port isn't in our list of allowed CDN ports if event.port not in self.allowed_cdn_ports: for cdn_tag in self.cdn_tags: - # and if any of the event's tags match our CDN filter - if any(t.startswith(str(cdn_tag)) for t in event.tags): + if cdn_tag in event.tags: return ( False, - f"one of the event's tags matches the tag '{cdn_tag}' and the port is not in the allowed list", + f"event has tag '{cdn_tag}' and the port is not in the allowed list", ) return True diff --git a/bbot/modules/portscan.py b/bbot/modules/portscan.py index 314ee2f94f..c4897d507b 100644 --- a/bbot/modules/portscan.py +++ b/bbot/modules/portscan.py @@ -10,7 +10,7 @@ class portscan(BaseModule): - flags = ["active", "portscan", "safe"] + flags = ["loud", "active", "portscan"] watched_events = ["IP_ADDRESS", "IP_RANGE", "DNS_NAME"] produced_events = ["OPEN_TCP_PORT"] meta = { @@ -126,7 +126,7 @@ async def masscan(self, targets, correlator, ping=False): with open(stats_file, "w") as stats_fh: async for line in self.run_process_live(command, sudo=True, stderr=stats_fh): for ip, port in self.parse_json_line(line): - parent_events = correlator.search(ip) + parent_events = correlator.search(str(ip)) # masscan gets the occasional junk result. this is harmless and # seems to be a side effect of it having its own TCP stack # see https://github.com/robertdavidgraham/masscan/issues/397 @@ -152,7 +152,7 @@ async def make_targets(self, events, scanned_tracker): """ correlator = RadixTarget() targets = set() - for event in sorted(events, key=lambda e: host_size_key(e.host)): + for event in sorted(events, key=lambda e: host_size_key(str(e.host))): # skip events without host if not event.host: continue @@ -184,16 +184,17 @@ async def make_targets(self, events, scanned_tracker): await self.emit_open_port(event.host, port, event) # build a correlation from the IP back to its original parent event - events_set = correlator.search(ip) + ip_str = str(ip) + events_set = correlator.search(ip_str) if events_set is None: - correlator.insert(ip, {event}) + correlator.insert(ip_str, {event}) else: events_set.add(event) # has this IP already been scanned? - if not scanned_tracker.get(ip): + if not scanned_tracker.get(ip_str): # if not, add it to targets! - scanned_tracker.add(ip) + scanned_tracker.add(ip_str) targets.add(ip) else: self.debug(f"Skipping {ip} because it's already been scanned") @@ -220,7 +221,7 @@ async def emit_open_port(self, ip, port, parent_event): event_data, event_type, parent=parent_event, - context=f"{{module}} executed a {scan_type} scan against {parent_event.data} and found: {{event.type}}: {{event.data}}", + context=f"{{module}} executed a {scan_type} scan against {parent_event.data} and found: {{event.type}}: {{event.pretty_string}}", ) await self.emit_event(event) diff --git a/bbot/modules/postman.py b/bbot/modules/postman.py index f6eafc6ff9..ee0bf25cd2 100644 --- a/bbot/modules/postman.py +++ b/bbot/modules/postman.py @@ -4,7 +4,7 @@ class postman(postman): watched_events = ["ORG_STUB", "SOCIAL"] produced_events = ["CODE_REPOSITORY"] - flags = ["passive", "subdomain-enum", "safe", "code-enum"] + flags = ["safe", "passive", "subdomain-enum", "code-enum"] meta = { "description": "Query Postman's API for related workspaces, collections, requests and download them", "created_date": "2024-09-07", diff --git a/bbot/modules/postman_download.py b/bbot/modules/postman_download.py index bb4fa4d988..77644435ad 100644 --- a/bbot/modules/postman_download.py +++ b/bbot/modules/postman_download.py @@ -7,7 +7,7 @@ class postman_download(postman): watched_events = ["CODE_REPOSITORY"] produced_events = ["FILESYSTEM"] - flags = ["passive", "subdomain-enum", "safe", "code-enum", "download"] + flags = ["safe", "passive", "subdomain-enum", "code-enum", "download"] meta = { "description": "Download workspaces, collections, requests from Postman", "created_date": "2024-09-07", @@ -36,7 +36,7 @@ async def filter_event(self, event): return True async def handle_event(self, event): - repo_url = event.data.get("url") + repo_url = event.url workspace_id = await self.get_workspace_id(repo_url) if workspace_id: self.verbose(f"Found workspace ID {workspace_id} for {repo_url}") diff --git a/bbot/modules/rapiddns.py b/bbot/modules/rapiddns.py index 150728eca3..1e0667b70e 100644 --- a/bbot/modules/rapiddns.py +++ b/bbot/modules/rapiddns.py @@ -2,7 +2,7 @@ class rapiddns(subdomain_enum): - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { diff --git a/bbot/modules/reflected_parameters.py b/bbot/modules/reflected_parameters.py index f7e17e57e6..460d1f96f9 100644 --- a/bbot/modules/reflected_parameters.py +++ b/bbot/modules/reflected_parameters.py @@ -4,7 +4,7 @@ class reflected_parameters(BaseModule): watched_events = ["WEB_PARAMETER"] produced_events = ["FINDING"] - flags = ["active", "safe", "web-thorough"] + flags = ["safe", "active", "web-heavy"] meta = { "description": "Highlight parameters that reflect their contents in response body", "author": "@liquidsec", @@ -12,7 +12,7 @@ class reflected_parameters(BaseModule): } async def handle_event(self, event): - url = event.data.get("url") + url = event.url reflection_detected = await self.detect_reflection(event, url) if reflection_detected: @@ -25,7 +25,14 @@ async def handle_event(self, event): description += ( f" Original Value: [{self.helpers.truncate_string(str(event.data['original_value']), 200)}]" ) - data = {"host": str(event.host), "description": description, "url": url} + data = { + "host": str(event.host), + "description": description, + "url": url, + "name": "Reflected Parameter", + "severity": "INFO", + "confidence": "HIGH", + } await self.emit_event(data, "FINDING", event) async def detect_reflection(self, event, url): @@ -51,22 +58,23 @@ async def detect_reflection(self, event, url): async def send_probe_with_canary(self, event, parameter_name, parameter_value, canary_value, cookies, timeout=10): method = "GET" - url = event.data["url"] + url = event.url headers = {} data = None json_data = None params = {parameter_name: parameter_value, "c4n4ry": canary_value} + param_type = event.data["type"] - if event.data["type"] == "GETPARAM": + if param_type == "GETPARAM": url = f"{url}?{parameter_name}={parameter_value}&c4n4ry={canary_value}" - elif event.data["type"] == "COOKIE": + elif param_type == "COOKIE": cookies.update(params) - elif event.data["type"] == "HEADER": + elif param_type == "HEADER": headers.update(params) - elif event.data["type"] == "POSTPARAM": + elif param_type == "POSTPARAM": method = "POST" data = params - elif event.data["type"] == "BODYJSON": + elif param_type == "BODYJSON": method = "POST" json_data = params diff --git a/bbot/modules/report/affiliates.py b/bbot/modules/report/affiliates.py index a67c665504..59ca9a25b0 100644 --- a/bbot/modules/report/affiliates.py +++ b/bbot/modules/report/affiliates.py @@ -4,7 +4,7 @@ class affiliates(BaseReportModule): watched_events = ["*"] produced_events = [] - flags = ["passive", "safe", "affiliates"] + flags = ["safe", "passive", "affiliates"] meta = { "description": "Summarize affiliate domains at the end of a scan", "created_date": "2022-07-25", diff --git a/bbot/modules/report/asn.py b/bbot/modules/report/asn.py index 3b3c488d15..54455e3bf0 100644 --- a/bbot/modules/report/asn.py +++ b/bbot/modules/report/asn.py @@ -1,12 +1,13 @@ from bbot.modules.report.base import BaseReportModule +from bbot.core.helpers.asn import ASNHelper class asn(BaseReportModule): watched_events = ["IP_ADDRESS"] produced_events = ["ASN"] - flags = ["passive", "subdomain-enum", "safe"] + flags = ["safe", "passive", "subdomain-enum"] meta = { - "description": "Query ripe and bgpview.io for ASNs", + "description": "Query asndb for ASN information", "created_date": "2022-07-25", "author": "@TheTechromancer", } @@ -16,17 +17,9 @@ class asn(BaseReportModule): accept_dupes = True async def setup(self): - self.asn_counts = {} - self.asn_cache = {} - self.ripe_cache = {} - self.sources = ["bgpview", "ripe"] - self.unknown_asn = { - "asn": "UNKNOWN", - "subnet": "0.0.0.0/32", - "name": "unknown", - "description": "unknown", - "country": "", - } + self.unknown_asn = ASNHelper.UNKNOWN_ASN + # Track ASN counts locally for reporting + self.asn_counts = {} # ASN number -> count mapping return True async def filter_event(self, event): @@ -38,215 +31,60 @@ async def filter_event(self, event): async def handle_event(self, event): host = event.host - if self.cache_get(host) is False: - asns, source = await self.get_asn(host) - if not asns: - self.cache_put(self.unknown_asn) - else: - for asn in asns: - emails = asn.pop("emails", []) - self.cache_put(asn) - asn_event = self.make_event(asn, "ASN", parent=event) - asn_number = asn.get("asn", "") - asn_desc = asn.get("description", "") - asn_name = asn.get("name", "") - asn_subnet = asn.get("subnet", "") - if not asn_event: - continue + host_str = str(host) + + asn_data = await self.helpers.asn.ip_to_subnets(host_str) + if asn_data: + asn_number = asn_data.get("asn", 0) + asn_description = asn_data.get("description", "") + asn_name = asn_data.get("name", "") + asn_country = asn_data.get("country", "") + subnets = asn_data.get("subnets", []) + + # Track ASN subnet counts for reporting (only once per ASN) + if asn_number and asn_number != 0: + if asn_number not in self.asn_counts: + self.asn_counts[asn_number] = len(subnets) + + # Don't emit ASN 0 - it's reserved and indicates unknown ASN data + if asn_number != 0: + asn_event = self.make_event(asn_number, "ASN", parent=event) + if asn_event: await self.emit_event( asn_event, - context=f"{{module}} checked {event.data} against {source} API and got {{event.type}}: AS{asn_number} ({asn_name}, {asn_desc}, {asn_subnet})", + context=f"{{module}} looked up {event.pretty_string} and got {{event.type}}: AS{asn_number} ({asn_name}, {asn_description}, {asn_country})", ) - for email in emails: - await self.emit_event( - email, - "EMAIL_ADDRESS", - parent=asn_event, - context=f"{{module}} retrieved details for AS{asn_number} and found {{event.type}}: {{event.data}}", - ) async def report(self): - asn_data = sorted(self.asn_cache.items(), key=lambda x: self.asn_counts[x[0]], reverse=True) - if not asn_data: - return - header = ["ASN", "Subnet", "Host Count", "Name", "Description", "Country"] - table = [] - for subnet, asn in asn_data: - count = self.asn_counts[subnet] - number = asn["asn"] - if number != "UNKNOWN": - number = "AS" + number - name = asn["name"] - country = asn["country"] - description = asn["description"] - table.append([number, str(subnet), f"{count:,}", name, description, country]) - self.log_table(table, header, table_name="asns") - - def cache_put(self, asn): - asn = dict(asn) - subnet = self.helpers.make_ip_type(asn.pop("subnet")) - self.asn_cache[subnet] = asn - try: - self.asn_counts[subnet] += 1 - except KeyError: - self.asn_counts[subnet] = 1 + """Generate an ASN summary table based on locally tracked ASN counts.""" - def cache_get(self, ip): - ret = False - for p in self.helpers.ip_network_parents(ip): - try: - self.asn_counts[p] += 1 - if ret is False: - ret = p - except KeyError: - continue - return ret + if not self.asn_counts: + return - async def get_asn(self, ip, retries=1): - """ - Takes in an IP - returns a list of ASNs, e.g.: - [{'asn': '54113', 'subnet': '2606:50c0:8000::/48', 'name': 'FASTLY', 'description': 'Fastly', 'country': 'US', 'emails': []}, {'asn': '54113', 'subnet': '2606:50c0:8000::/46', 'name': 'FASTLY', 'description': 'Fastly', 'country': 'US', 'emails': []}] - """ - for attempt in range(retries + 1): - for i, source in enumerate(list(self.sources)): - get_asn_fn = getattr(self, f"get_asn_{source}") - res = await get_asn_fn(ip) - if res is False: - # demote the current source to lowest priority since it just failed - self.sources.append(self.sources.pop(i)) - self.verbose(f"Failed to contact {source}, retrying") - continue - return res, source - self.warning(f"Error retrieving ASN for {ip}") - return [], "" + # Build table rows sorted by ASN number (low to high) + sorted_asns = sorted(self.asn_counts.items(), key=lambda x: int(x[0])) - async def get_asn_ripe(self, ip): - url = f"https://stat.ripe.net/data/network-info/data.json?resource={ip}" - response = await self.get_url(url, "ASN") - asns = [] - if response is False: - return False - data = response.get("data", {}) - if not data: - data = {} - prefix = data.get("prefix", "") - asn_numbers = data.get("asns", []) - if not prefix or not asn_numbers: - return [] - if not asn_numbers: - asn_numbers = [] - for number in asn_numbers: - asn = await self.get_asn_metadata_ripe(number) - if asn is False: - return False - asn["subnet"] = prefix - asns.append(asn) - return asns - - async def get_asn_metadata_ripe(self, asn_number): - try: - return self.ripe_cache[asn_number] - except KeyError: - metadata_keys = { - "name": ["ASName", "OrgId"], - "description": ["OrgName", "OrgTechName", "RTechName"], - "country": ["Country"], - } - url = f"https://stat.ripe.net/data/whois/data.json?resource={asn_number}" - response = await self.get_url(url, "ASN Metadata", cache=True) - if response is False: - return False - data = response.get("data", {}) - if not data: - data = {} - records = data.get("records", []) - if not records: - records = [] - emails = set() - asn = {k: "" for k in metadata_keys.keys()} - for record in records: - for item in record: - key = item.get("key", "") - value = item.get("value", "") - for email in await self.helpers.re.extract_emails(value): - emails.add(email.lower()) - if not key: - continue - if value: - for keyname, keyvals in metadata_keys.items(): - if key in keyvals and not asn.get(keyname, ""): - asn[keyname] = value - asn["emails"] = list(emails) - asn["asn"] = str(asn_number) - self.ripe_cache[asn_number] = asn - return asn + header = ["ASN", "Subnet Count", "Name", "Description", "Country"] + table = [] + for asn_number, subnet_count in sorted_asns: + # Get ASN details from helper + asn_data = await self.helpers.asn.asn_to_subnets(asn_number) + if asn_data: + asn_name = asn_data.get("name", "") + asn_description = asn_data.get("description", "") + asn_country = asn_data.get("country", "") + else: + asn_name = asn_description = asn_country = "unknown" - async def get_asn_bgpview(self, ip): - url = f"https://api.bgpview.io/ip/{ip}" - data = await self.get_url(url, "ASN") - asns = [] - asns_tried = set() - if data is False: - return False - data = data.get("data", {}) - prefixes = data.get("prefixes", []) - for prefix in prefixes: - details = prefix.get("asn", {}) - asn = str(details.get("asn", "")) - subnet = prefix.get("prefix", "") - if not (asn or subnet): - continue - name = details.get("name") or prefix.get("name") or "" - description = details.get("description") or prefix.get("description") or "" - country = details.get("country_code") or prefix.get("country_code") or "" - emails = [] - if asn not in asns_tried: - emails = await self.get_emails_bgpview(asn) - if emails is False: - return False - asns_tried.add(asn) - asns.append( - { - "asn": asn, - "subnet": subnet, - "name": name, - "description": description, - "country": country, - "emails": emails, - } + number = f"AS{asn_number}" if asn_number != 0 else str(asn_number) + table.append( + [ + number, + f"{subnet_count:,}", + asn_name, + asn_description, + asn_country, + ] ) - if not asns: - self.debug(f'No results for "{ip}"') - return asns - async def get_emails_bgpview(self, asn): - contacts = [] - url = f"https://api.bgpview.io/asn/{asn}" - data = await self.get_url(url, "ASN metadata", cache=True) - if data is False: - return False - data = data.get("data", {}) - if not data: - self.debug(f'No results for "{asn}"') - return - email_contacts = data.get("email_contacts", []) - abuse_contacts = data.get("abuse_contacts", []) - contacts = [l.strip().lower() for l in email_contacts + abuse_contacts] - return list(set(contacts)) - - async def get_url(self, url, data_type, cache=False): - kwargs = {} - if cache: - kwargs["cache_for"] = 60 * 60 * 24 - r = await self.helpers.request(url, **kwargs) - data = {} - try: - j = r.json() - if not isinstance(j, dict): - return data - return j - except Exception as e: - self.verbose(f"Error retrieving {data_type} at {url}: {e}", trace=True) - self.debug(f"Got data: {getattr(r, 'content', '')}") - return False + self.log_table(table, header, table_name="asns") diff --git a/bbot/modules/retirejs.py b/bbot/modules/retirejs.py index 27e8fec407..78fc72dfed 100644 --- a/bbot/modules/retirejs.py +++ b/bbot/modules/retirejs.py @@ -21,7 +21,7 @@ def from_string(cls, severity_str): class retirejs(BaseModule): watched_events = ["URL_UNVERIFIED"] produced_events = ["FINDING"] - flags = ["active", "safe", "web-thorough"] + flags = ["safe", "active", "web-heavy"] meta = { "description": "Detect vulnerable/out-of-date JavaScript libraries", "created_date": "2025-08-19", @@ -133,7 +133,7 @@ async def setup(self): return True async def handle_event(self, event): - js_file = await self.helpers.request(event.data) + js_file = await self.helpers.request(event.url) if js_file: js_file_body = js_file.text if js_file_body: @@ -168,7 +168,7 @@ async def handle_event(self, event): f"Vulnerable JavaScript library detected: {component} v{version}", f"Severity: {severity.upper()}", f"Summary: {summary}", - f"JavaScript URL: {event.data}", + f"JavaScript URL: {event.url}", ] if cves: description_parts.append(f"CVE(s): {', '.join(cves)}") @@ -183,10 +183,12 @@ async def handle_event(self, event): description_parts.append(f"Affected versions: [>= {at_or_above}]") description = " ".join(description_parts) data = { + "name": "Vulnerable JavaScript Library", "description": description, "severity": severity, + "confidence": "HIGH", "component": component, - "url": event.parent.data["url"], + "url": event.parent.url, } await self.emit_event( data, diff --git a/bbot/modules/robots.py b/bbot/modules/robots.py index e41b3119fb..1240f11959 100644 --- a/bbot/modules/robots.py +++ b/bbot/modules/robots.py @@ -4,7 +4,7 @@ class robots(BaseModule): watched_events = ["URL"] produced_events = ["URL_UNVERIFIED"] - flags = ["active", "safe", "web-basic"] + flags = ["safe", "active", "web"] meta = {"description": "Look for and parse robots.txt", "created_date": "2023-02-01", "author": "@liquidsec"} options = {"include_sitemap": False, "include_allow": True, "include_disallow": True} @@ -49,5 +49,5 @@ async def handle_event(self, event): "URL_UNVERIFIED", parent=event, tags=["spider-danger"], - context=f"{{module}} found robots.txt at {url} and extracted {{event.type}}: {{event.data}}", + context=f"{{module}} found robots.txt at {url} and extracted {{event.type}}: {{event.pretty_string}}", ) diff --git a/bbot/modules/securitytrails.py b/bbot/modules/securitytrails.py index b92ac07dc1..7ab333ee76 100644 --- a/bbot/modules/securitytrails.py +++ b/bbot/modules/securitytrails.py @@ -4,7 +4,7 @@ class securitytrails(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query the SecurityTrails API for subdomains", "created_date": "2022-07-03", diff --git a/bbot/modules/securitytxt.py b/bbot/modules/securitytxt.py index d7e26c0b5f..d8d3d4aea2 100644 --- a/bbot/modules/securitytxt.py +++ b/bbot/modules/securitytxt.py @@ -58,7 +58,7 @@ class securitytxt(BaseModule): watched_events = ["DNS_NAME"] produced_events = ["EMAIL_ADDRESS", "URL_UNVERIFIED"] - flags = ["subdomain-enum", "cloud-enum", "active", "web-basic", "safe"] + flags = ["safe", "subdomain-enum", "cloud-enum", "active", "web"] meta = { "description": "Check for security.txt content", "author": "@colin-stubbs", diff --git a/bbot/modules/shodan_dns.py b/bbot/modules/shodan_dns.py index 2ad0bc5057..4e68ed11d4 100644 --- a/bbot/modules/shodan_dns.py +++ b/bbot/modules/shodan_dns.py @@ -4,7 +4,7 @@ class shodan_dns(shodan): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query Shodan for subdomains", "created_date": "2022-07-03", diff --git a/bbot/modules/shodan_enterprise.py b/bbot/modules/shodan_enterprise.py new file mode 100644 index 0000000000..273fe837da --- /dev/null +++ b/bbot/modules/shodan_enterprise.py @@ -0,0 +1,156 @@ +from bbot.modules.base import BaseModule + + +class shodan_enterprise(BaseModule): + watched_events = ["IP_ADDRESS"] + produced_events = ["OPEN_TCP_PORT", "TECHNOLOGY", "OPEN_UDP_PORT", "FINDING"] + flags = ["safe", "passive"] + meta = { + "created_date": "2026-01-27", + "author": "@Control-Punk-Delete", + "description": "Shodan Enterprise API integration module.", + "auth_required": True, + } + options = {"api_key": "", "in_scope_only": True} + options_desc = { + "api_key": "Shodan API Key", + "in_scope_only": "Only query in-scope IPs. If False, will query up to distance 1.", + } + in_scope_only = True + + base_url = "https://api.shodan.io" + + async def setup(self): + self.api_key = self.config.get("api_key", "") + if not self.api_key: + return None, "No API key specified" + if not self.config.get("in_scope_only", True): + self.in_scope_only = False + self.scope_distance_modifier = 1 + self.warning( + "in_scope_only is disabled. This module queries each IP individually and may consume a lot of API credits!" + ) + return True + + async def handle_event(self, event): + ip = event.data + url = f"{self.base_url}/shodan/host/{self.helpers.quote(ip)}?key={{api_key}}" + r = await self.api_request(url) + if r is None: + self.warning(f"No response from Shodan API for {ip}") + return + status_code = getattr(r, "status_code", 0) + if status_code == 404: + self.warning(f"No Shodan data about {ip}") + return + if not getattr(r, "is_success", False): + self.warning(f"Shodan API error for {ip} (status {status_code})") + return + try: + host = r.json() + except Exception as e: + self.warning(f"Failed to parse Shodan API response for {ip}: {e}") + return + + if "data" not in host: + self.warning(f"No Shodan data about {ip}") + return + + # NIST cvss score severity mapping + severity_map = {"NONE": 0.0, "LOW": 0.1, "MEDIUM": 4.0, "HIGH": 7.0, "CRITICAL": 9.0} + + for data in host["data"]: + # TECHNOLOGY Extraction + ## TECHNOLOGY CPE Formats + for technology in data.get("cpe", []): + tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} + await self.emit_event( + tech, + "TECHNOLOGY", + parent=event, + tags=data.get("tags") or [], + context=f"{{module}} queried Shodan API for {ip} and found TECHNOLOGY: {technology}", + ) + + for technology in data.get("cpe23", []): + tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} + await self.emit_event( + tech, + "TECHNOLOGY", + parent=event, + tags=data.get("tags") or [], + context=f"{{module}} queried Shodan API for {ip} and found TECHNOLOGY: {technology}", + ) + + # TECHNOLOGY Additional Formats + if "product" in data: + tech = { + "technology": data.get("product"), + "host": data.get("ip_str"), + "port": data.get("port"), + } + await self.emit_event( + tech, + "TECHNOLOGY", + parent=event, + tags=data.get("tags") or [], + context=f"{{module}} queried Shodan API for {ip} and found TECHNOLOGY: {data['product']}", + ) + + if "http" in data: + if "components" in data["http"]: + for technology in data["http"]["components"]: + tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} + tags = list(data["http"]["components"][technology].get("categories", [])) + tags.append("web-technology") + await self.emit_event( + tech, + "TECHNOLOGY", + parent=event, + tags=tags, + context=f"{{module}} queried Shodan API for {ip} and found TECHNOLOGY: {technology}", + ) + + # OPEN_TCP_PORT, OPEN_UDP_PORT Extraction + if "port" in data and "transport" in data: + if data["transport"] == "tcp": + await self.emit_event( + self.helpers.make_netloc(ip, data.get("port")), + "OPEN_TCP_PORT", + parent=event, + tags=data.get("tags") or [], + context=f"{{module}} queried Shodan API for {ip} and found OPEN_TCP_PORT: {data.get('port')}", + ) + elif data["transport"] == "udp": + await self.emit_event( + self.helpers.make_netloc(ip, data.get("port")), + "OPEN_UDP_PORT", + parent=event, + tags=data.get("tags") or [], + context=f"{{module}} queried Shodan API for {ip} and found OPEN_UDP_PORT: {data.get('port')}", + ) + else: + self.warning(f"Unknown transport {data['transport']}") + + # FINDING Extraction + if "vulns" in data: + for cve, vuln_data in data["vulns"].items(): + cvss = vuln_data.get("cvss", 0) + severity = max( + (level for level, threshold in severity_map.items() if cvss >= threshold), + key=lambda x: severity_map[x], + ) + vuln = { + "name": "Shodan - Possible Vulnerabilities", + "host": data.get("ip_str"), + "severity": severity, + "description": cve, + "confidence": "LOW", + } + await self.emit_event( + vuln, + "FINDING", + parent=event, + tags=[], + context=f"{{module}} queried Shodan API for {ip} and found FINDING {cve}", + ) diff --git a/bbot/modules/shodan_idb.py b/bbot/modules/shodan_idb.py index 4a3e2b214a..59ad329d35 100644 --- a/bbot/modules/shodan_idb.py +++ b/bbot/modules/shodan_idb.py @@ -40,8 +40,8 @@ class shodan_idb(BaseModule): """ watched_events = ["IP_ADDRESS", "DNS_NAME"] - produced_events = ["TECHNOLOGY", "VULNERABILITY", "FINDING", "OPEN_TCP_PORT", "DNS_NAME"] - flags = ["passive", "safe", "portscan", "subdomain-enum"] + produced_events = ["TECHNOLOGY", "FINDING", "OPEN_TCP_PORT", "DNS_NAME"] + flags = ["safe", "passive", "portscan", "subdomain-enum"] meta = { "description": "Query Shodan's InternetDB for open ports, hostnames, technologies, and vulnerabilities", "created_date": "2023-12-22", @@ -91,7 +91,7 @@ async def handle_event(self, event): r = await self.api_request(url) if r is None: - self.debug(f"No response for {event.data}") + self.debug(f"No response for {event.pretty_string}") return try: data = r.json() @@ -115,7 +115,7 @@ async def _parse_response(self, data: dict, event, ip): """Handles emitting events from returned JSON""" data: dict # has keys: cpes, hostnames, ip, ports, tags, vulns ip = str(ip) - query_host = ip if event.data == ip else f"{event.data} ({ip})" + query_host = ip if event.data == ip else f"{event.pretty_string} ({ip})" # ip is a string, ports is a list of ports, the rest is a list of strings for hostname in data.get("hostnames", []): if hostname != event.data: @@ -123,27 +123,34 @@ async def _parse_response(self, data: dict, event, ip): hostname, "DNS_NAME", parent=event, - context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found {{event.type}}: {{event.pretty_string}}', ) for cpe in data.get("cpes", []): await self.emit_event( {"technology": cpe, "host": str(event.host)}, "TECHNOLOGY", parent=event, - context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found {{event.type}}: {{event.pretty_string}}', ) for port in data.get("ports", []): await self.emit_event( self.helpers.make_netloc(event.data, port), "OPEN_TCP_PORT", parent=event, - context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found {{event.type}}: {{event.pretty_string}}', ) vulns = data.get("vulns", []) if vulns: vulns_str = ", ".join([str(v) for v in vulns]) await self.emit_event( - {"description": f"Shodan reported possible vulnerabilities: {vulns_str}", "host": str(event.host)}, + { + "description": f"Shodan reported possible vulnerabilities: {vulns_str}", + "host": str(event.host), + "cves": vulns, + "name": "Shodan - Possible Vulnerabilities", + "severity": "MEDIUM", + "confidence": "LOW", + }, "FINDING", parent=event, context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found potential {{event.type}}: {vulns_str}', diff --git a/bbot/modules/sitedossier.py b/bbot/modules/sitedossier.py index 187aae1941..b4f19b701b 100644 --- a/bbot/modules/sitedossier.py +++ b/bbot/modules/sitedossier.py @@ -2,7 +2,7 @@ class sitedossier(subdomain_enum): - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { @@ -28,7 +28,7 @@ async def handle_event(self, event): "DNS_NAME", event, abort_if=self.abort_if, - context=f'{{module}} searched sitedossier.com for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched sitedossier.com for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) async def query(self, query, parse_fn=None, request_fn=None): diff --git a/bbot/modules/skymem.py b/bbot/modules/skymem.py index 6f39a6c0d8..4fd3e072ab 100644 --- a/bbot/modules/skymem.py +++ b/bbot/modules/skymem.py @@ -6,7 +6,7 @@ class skymem(emailformat): watched_events = ["DNS_NAME"] produced_events = ["EMAIL_ADDRESS"] - flags = ["passive", "email-enum", "safe"] + flags = ["safe", "passive", "email-enum"] meta = { "description": "Query skymem.info for email addresses", "created_date": "2022-07-11", @@ -51,5 +51,5 @@ async def handle_event(self, event): email, "EMAIL_ADDRESS", parent=event, - context=f'{{module}} searched skymem.info for "{query}" and found {{event.type}} on page {i + 1}: {{event.data}}', + context=f'{{module}} searched skymem.info for "{query}" and found {{event.type}} on page {i + 1}: {{event.pretty_string}}', ) diff --git a/bbot/modules/smuggler.py b/bbot/modules/smuggler.py index 357fec1885..5964a3bf99 100644 --- a/bbot/modules/smuggler.py +++ b/bbot/modules/smuggler.py @@ -11,7 +11,7 @@ class smuggler(BaseModule): watched_events = ["URL"] produced_events = ["FINDING"] - flags = ["active", "aggressive", "slow", "web-thorough"] + flags = ["active", "loud", "invasive", "slow", "web-heavy"] meta = {"description": "Check for HTTP smuggling", "created_date": "2022-07-06", "author": "@liquidsec"} in_scope_only = True @@ -31,7 +31,7 @@ async def handle_event(self, event): "--no-color", "-q", "-u", - event.data, + event.url, ] async for line in self.run_process_live(command): for f in line.split("\r"): @@ -40,8 +40,15 @@ async def handle_event(self, event): text = f.split(":")[1].split("-")[0].strip() description = f"[HTTP SMUGGLER] [{text}] Technique: {technique}" await self.emit_event( - {"host": str(event.host), "url": event.data, "description": description}, + { + "host": str(event.host), + "url": event.url, + "description": description, + "name": "Possible HTTP Smuggling", + "severity": "MEDIUM", + "confidence": "LOW", + }, "FINDING", parent=event, - context=f"{{module}} scanned {event.data} and found HTTP smuggling ({{event.type}}): {text}", + context=f"{{module}} scanned {event.url} and found HTTP smuggling ({{event.type}}): {text}", ) diff --git a/bbot/modules/social.py b/bbot/modules/social.py index fb46dd3870..f7b6ffc89e 100644 --- a/bbot/modules/social.py +++ b/bbot/modules/social.py @@ -10,7 +10,7 @@ class social(BaseModule): "created_date": "2023-03-28", "author": "@TheTechromancer", } - flags = ["passive", "safe", "social-enum"] + flags = ["safe", "passive", "social-enum"] # platform name : (regex, case_sensitive) social_media_platforms = { @@ -36,7 +36,7 @@ async def setup(self): async def handle_event(self, event): for platform, (regex, case_sensitive) in self.compiled_regexes.items(): - for match in regex.finditer(event.data): + for match in regex.finditer(event.url): url = match.group() profile_name = match.groups()[0] if not case_sensitive: diff --git a/bbot/modules/sslcert.py b/bbot/modules/sslcert.py index 3c52cf64fe..ff6f6cf402 100644 --- a/bbot/modules/sslcert.py +++ b/bbot/modules/sslcert.py @@ -11,7 +11,7 @@ class sslcert(BaseModule): watched_events = ["OPEN_TCP_PORT"] produced_events = ["DNS_NAME", "EMAIL_ADDRESS"] - flags = ["affiliates", "subdomain-enum", "email-enum", "active", "safe", "web-basic"] + flags = ["safe", "affiliates", "subdomain-enum", "email-enum", "active", "web"] meta = { "description": "Visit open ports and retrieve SSL certificates", "created_date": "2022-03-30", @@ -63,9 +63,9 @@ async def handle_event(self, event): else: abort_threshold = self.out_of_scope_abort_threshold - tasks = [self.visit_host(host, port) for host in hosts] - async for task in self.helpers.as_completed(tasks): - result = await task + coroutines = [self.visit_host(host, port) for host in hosts] + async for coroutine in self.helpers.as_completed(coroutines): + result = await coroutine if not isinstance(result, tuple) or not len(result) == 3: continue dns_names, emails, (host, port) = result @@ -90,10 +90,10 @@ async def handle_event(self, event): await self.emit_event( ssl_event, tags=tags, - context=f"{{module}} parsed SSL certificate at {event.data} and found {{event.type}}: {{event.data}}", + context=f"{{module}} parsed SSL certificate at {event.pretty_string} and found {{event.type}}: {{event.pretty_string}}", ) except ValidationError as e: - self.hugeinfo(f'Malformed {event_type} "{event_data}" at {event.data}') + self.hugeinfo(f'Malformed {event_type} "{event_data}" at {event.pretty_string}') self.debug(f"Invalid data at {host}:{port}: {e}") def on_success_callback(self, event): diff --git a/bbot/modules/subdomaincenter.py b/bbot/modules/subdomaincenter.py index 614be03ef8..7079d5942d 100644 --- a/bbot/modules/subdomaincenter.py +++ b/bbot/modules/subdomaincenter.py @@ -2,7 +2,7 @@ class subdomaincenter(subdomain_enum): - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { diff --git a/bbot/modules/subdomainradar.py b/bbot/modules/subdomainradar.py index 2a4c987948..eaca2374c5 100644 --- a/bbot/modules/subdomainradar.py +++ b/bbot/modules/subdomainradar.py @@ -7,7 +7,7 @@ class SubdomainRadar(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query the Subdomain API for subdomains", "created_date": "2022-07-08", @@ -64,6 +64,8 @@ async def setup(self): self.enum_tasks = {} self.poll_task = asyncio.create_task(self.task_poll_loop()) + # Track poll_task so _cancel_tasks() picks it up during shutdown + self._tasks.append(self.poll_task) return True @@ -124,7 +126,7 @@ async def parse_response(self, response, query, event): "DNS_NAME", event, abort_if=self.abort_if, - context=f'{{module}} searched SubDomainRadar.io API for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched SubDomainRadar.io API for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) return True return False diff --git a/bbot/modules/telerik.py b/bbot/modules/telerik.py index 3cd0c8eed9..493117fc7d 100644 --- a/bbot/modules/telerik.py +++ b/bbot/modules/telerik.py @@ -20,8 +20,8 @@ class telerik(BaseModule): """ watched_events = ["URL", "HTTP_RESPONSE"] - produced_events = ["VULNERABILITY", "FINDING"] - flags = ["active", "aggressive", "web-thorough"] + produced_events = ["FINDING"] + flags = ["active", "loud", "invasive", "web-heavy"] meta = { "description": "Scan for critical Telerik vulnerabilities", "created_date": "2022-04-10", @@ -186,16 +186,16 @@ def normalize_url(url): def _incoming_dedup_hash(self, event): if event.type == "URL": if self.config.get("include_subdirs") is True: - return hash(f"{event.type}{self.normalize_url(event.data)}") + return hash(f"{event.type}{self.normalize_url(event.url)}") else: return hash(f"{event.type}{event.netloc}") else: # HTTP_RESPONSE - return hash(f"{event.type}{event.data['url']}") + return hash(f"{event.type}{event.url}") async def handle_event(self, event): if event.type == "URL": if self.config.get("include_subdirs"): - base_url = self.normalize_url(event.data) # Use the entire URL including subdirectories + base_url = self.normalize_url(event.url) # Use the entire URL including subdirectories else: base_url = f"{event.parsed_url.scheme}://{event.parsed_url.netloc}/" # path will be omitted @@ -226,7 +226,7 @@ async def handle_event(self, event): verbose_errors = False # send probe probe_response = await self.helpers.request( - f"{event.data}{webresource}", method="POST", files=probe_data + f"{event.url}{webresource}", method="POST", files=probe_data ) if probe_response: @@ -242,7 +242,14 @@ async def handle_event(self, event): description = f"Telerik RAU AXD Handler detected. Verbose Errors Enabled: [{str(verbose_errors)}] Version Guess: [{version}]" await self.emit_event( - {"host": str(event.host), "url": f"{base_url}{webresource}", "description": description}, + { + "host": str(event.host), + "url": f"{base_url}{webresource}", + "description": description, + "name": "Telerik Handler", + "severity": "INFO", + "confidence": "HIGH", + }, "FINDING", event, context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik RAU AXD Handler", @@ -269,17 +276,19 @@ async def handle_event(self, event): command.append(self.scan.http_proxy) output = await self.run_process(command) - description = f"[CVE-2017-11317] [{str(version)}] {webresource}" + description = f"Confirmed Vulnerable Telerik (version: {str(version)})" if "fileInfo" in output.stdout: self.debug(f"Confirmed Vulnerable Telerik (version: {str(version)}") await self.emit_event( { "severity": "CRITICAL", + "confidence": "CONFIRMED", "description": description, "host": str(event.host), "url": f"{base_url}{webresource}", + "name": "Telerik RCE", }, - "VULNERABILITY", + "FINDING", event, context=f"{{module}} scanned {base_url} and identified critical {{event.type}}: {description}", ) @@ -307,7 +316,14 @@ async def handle_event(self, event): self.debug(f"Detected Telerik UI instance ({dh})") description = "Telerik DialogHandler detected" await self.emit_event( - {"host": str(event.host), "url": f"{base_url}{dh}", "description": description}, + { + "host": str(event.host), + "url": f"{base_url}{dh}", + "description": description, + "name": "Telerik Handler", + "confidence": "CONFIRMED", + "severity": "INFO", + }, "FINDING", event, ) @@ -331,6 +347,9 @@ async def handle_event(self, event): "host": str(event.host), "url": f"{base_url}{spellcheckhandler}", "description": description, + "name": "Telerik Handler", + "confidence": "CONFIRMED", + "severity": "INFO", }, "FINDING", event, @@ -350,6 +369,9 @@ async def handle_event(self, event): "host": str(event.host), "url": f"{base_url}{chartimagehandler}", "description": "Telerik ChartImage AXD Handler Detected", + "name": "Telerik Handler", + "confidence": "CONFIRMED", + "severity": "INFO", }, "FINDING", event, @@ -358,7 +380,7 @@ async def handle_event(self, event): elif event.type == "HTTP_RESPONSE": resp_body = event.data.get("body", None) - url = event.data["url"] + url = event.url if resp_body: if '":{"SerializedParameters":"' in resp_body: await self.emit_event( @@ -366,6 +388,9 @@ async def handle_event(self, event): "host": str(event.host), "url": url, "description": "Telerik DialogHandler [SerializedParameters] Detected in HTTP Response", + "name": "Telerik Handler", + "confidence": "CONFIRMED", + "severity": "INFO", }, "FINDING", event, @@ -377,6 +402,9 @@ async def handle_event(self, event): "host": str(event.host), "url": url, "description": "Telerik AsyncUpload [serializedConfiguration] Detected in HTTP Response", + "name": "Telerik AsyncUpload", + "confidence": "CONFIRMED", + "severity": "INFO", }, "FINDING", event, diff --git a/bbot/modules/templates/bucket.py b/bbot/modules/templates/bucket.py index d5fdd2d3f9..2e3afcb4ac 100644 --- a/bbot/modules/templates/bucket.py +++ b/bbot/modules/templates/bucket.py @@ -7,7 +7,7 @@ class bucket_template(BaseModule): watched_events = ["DNS_NAME", "STORAGE_BUCKET"] produced_events = ["STORAGE_BUCKET", "FINDING"] - flags = ["active", "safe", "cloud-enum", "web-basic"] + flags = ["active", "cloud-enum", "web"] options = {"permutations": False} options_desc = { "permutations": "Whether to try permutations", @@ -42,7 +42,7 @@ async def filter_event(self, event): return True def filter_bucket(self, event): - if not any(t.endswith(f"-{self.cloudcheck_provider_name.lower()}") for t in event.tags): + if self.cloudcheck_provider_name.lower() not in event.tags: return False, "bucket belongs to a different cloud provider" return True, "" @@ -67,16 +67,23 @@ async def handle_dns_name(self, event): "STORAGE_BUCKET", parent=event, tags=tags, - context=f"{{module}} tried {num_buckets:,} bucket variations of {event.data} and found {{event.type}} at {url}", + context=f"{{module}} tried {num_buckets:,} bucket variations of {event.pretty_string} and found {{event.type}} at {url}", ) async def handle_storage_bucket(self, event): - url = event.data["url"] + url = event.url bucket_name = event.data["name"] if self.supports_open_check: description, tags = await self._check_bucket_open(bucket_name, url) if description: - event_data = {"host": event.host, "url": url, "description": description} + event_data = { + "host": event.host, + "url": url, + "description": description, + "name": "Open Storage Bucket", + "severity": "LOW", + "confidence": "HIGH", + } await self.emit_event( event_data, "FINDING", diff --git a/bbot/modules/templates/gitlab.py b/bbot/modules/templates/gitlab.py index 4b8605955f..d53ac12596 100644 --- a/bbot/modules/templates/gitlab.py +++ b/bbot/modules/templates/gitlab.py @@ -92,7 +92,7 @@ async def handle_namespace(self, namespace, event): # Utility helpers # ------------------------------------------------------------------ def get_base_url(self, event): - base_url = event.data.get("url", "") + base_url = event.url if not base_url: base_url = f"https://{event.host}" return self.helpers.urlparse(base_url)._replace(path="/").geturl() diff --git a/bbot/modules/templates/sql.py b/bbot/modules/templates/sql.py index 39b4e6f00e..ef12ef7bf7 100644 --- a/bbot/modules/templates/sql.py +++ b/bbot/modules/templates/sql.py @@ -1,9 +1,10 @@ +import asyncio from contextlib import suppress from sqlmodel import SQLModel from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from bbot.db.sql.models import Event, Scan, Target +from bbot.models.sql import Event, Scan, Target from bbot.modules.output.base import BaseOutputModule @@ -15,6 +16,7 @@ class SQLTemplate(BaseOutputModule): "password": "", "host": "127.0.0.1", "port": 0, + "retries": 10, } options_desc = { "database": "The database to use", @@ -22,6 +24,7 @@ class SQLTemplate(BaseOutputModule): "password": "The password to use to connect to the database", "host": "The host to use to connect to the database", "port": "The port to use to connect to the database", + "retries": "Number of times to retry connecting to the database (1 second between retries)", } protocol = "" @@ -32,9 +35,26 @@ async def setup(self): self.password = self.config.get("password", "") self.host = self.config.get("host", "127.0.0.1") self.port = self.config.get("port", 0) - - await self.init_database() - return True + retries = self.config.get("retries", 10) + + connection_string = self.connection_string(mask_password=True) + last_error = None + max_attempts = retries + 1 + for attempt in range(max_attempts): + try: + self.verbose(f"Connecting to {connection_string} (attempt {attempt + 1}/{max_attempts})") + await self.init_database() + self.verbose(f"Successfully connected to {connection_string}") + return True + except Exception as e: + last_error = e + if attempt < retries: + self.verbose( + f"Failed to connect to {connection_string} (attempt {attempt + 1}/{max_attempts}): {e}" + ) + await asyncio.sleep(1) + + return False, f"Failed to reach {connection_string} after {max_attempts} attempts: {last_error}" async def handle_event(self, event): event_obj = Event(**event.json()).validated diff --git a/bbot/modules/templates/subdomain_enum.py b/bbot/modules/templates/subdomain_enum.py index a65d08f315..3bdcdff07b 100644 --- a/bbot/modules/templates/subdomain_enum.py +++ b/bbot/modules/templates/subdomain_enum.py @@ -9,7 +9,7 @@ class subdomain_enum(BaseModule): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["subdomain-enum", "passive"] meta = {"description": "Query an API for subdomains"} base_url = "https://api.example.com" @@ -63,7 +63,7 @@ async def handle_event(self, event): "DNS_NAME", event, abort_if=self.abort_if, - context=f'{{module}} searched {self.source_pretty_name} for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched {self.source_pretty_name} for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) async def handle_event_paginated(self, event): @@ -81,7 +81,7 @@ async def handle_event_paginated(self, event): "DNS_NAME", event, abort_if=self.abort_if, - context=f'{{module}} searched {self.source_pretty_name} for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched {self.source_pretty_name} for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) async def request_url(self, query): @@ -166,10 +166,10 @@ async def filter_event(self, event): is_wildcard = await self._is_wildcard(query) # check if cloud is_cloud = False - if any(t.startswith("cloud-") for t in event.tags): + if "cloud" in event.tags: is_cloud = True - # reject if it's a cloud resource and not in our target - if is_cloud and event not in self.scan.target.whitelist: + # reject if it's a cloud resource and not in our target (unless it's a seed event) + if is_cloud and not self.scan.in_target(event) and "seed" not in event.tags: return False, "Event is a cloud resource and not a direct target" # optionally reject events with wildcards / errors if self.reject_wildcards: @@ -198,7 +198,7 @@ class subdomain_enum_apikey(subdomain_enum): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["subdomain-enum", "passive"] meta = {"description": "Query API for subdomains", "auth_required": True} options = {"api_key": ""} options_desc = {"api_key": "API key"} diff --git a/bbot/modules/templates/webhook.py b/bbot/modules/templates/webhook.py index 79dc11750d..9b71272be9 100644 --- a/bbot/modules/templates/webhook.py +++ b/bbot/modules/templates/webhook.py @@ -11,7 +11,8 @@ class WebhookOutputModule(BaseOutputModule): accept_dupes = False message_size_limit = 2000 content_key = "content" - vuln_severities = ["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] + severities = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"] + confidences = ["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CONFIRMED"] # abort module after 10 failed requests (not including retries) _api_failure_abort_threshold = 10 @@ -21,10 +22,10 @@ class WebhookOutputModule(BaseOutputModule): async def setup(self): self.webhook_url = self.config.get("webhook_url", "") self.min_severity = self.config.get("min_severity", "LOW").strip().upper() - assert self.min_severity in self.vuln_severities, ( - f"min_severity must be one of the following: {','.join(self.vuln_severities)}" + assert self.min_severity in self.severities, ( + f"min_severity must be one of the following: {','.join(self.severities)}" ) - self.allowed_severities = self.vuln_severities[self.vuln_severities.index(self.min_severity) :] + self.allowed_severities = self.severities[self.severities.index(self.min_severity) :] if not self.webhook_url: self.warning("Must set Webhook URL") return False @@ -45,37 +46,40 @@ async def handle_event(self, event): def get_watched_events(self): if self._watched_events is None: - event_types = self.config.get("event_types", ["VULNERABILITY"]) + event_types = self.config.get("event_types", ["FINDING"]) if isinstance(event_types, str): event_types = [event_types] self._watched_events = set(event_types) return self._watched_events async def filter_event(self, event): - if event.type == "VULNERABILITY": - severity = event.data.get("severity", "UNKNOWN") + if event.type == "FINDING": + severity = event.data.get("severity", "INFO") if severity not in self.allowed_severities: return False, f"{severity} is below min_severity threshold" return True def format_message_str(self, event): event_tags = ",".join(event.tags) - return f"`[{event.type}]`\t**`{event.data}`**\ttags:{event_tags}" + return f"`[{event.type}]`\t**`{event.pretty_string}`**\ttags:{event_tags}" def format_message_other(self, event): event_yaml = yaml.dump(event.data) event_type = f"**`[{event.type}]`**" - if event.type in ("VULNERABILITY", "FINDING"): - event_str, color = self.get_severity_color(event) - event_type = f"{color} {event_str} {color}" + if event.type == "FINDING": + event_str, severity_color, confidence_color = self.get_colors(event) + event_type = f"{severity_color} {confidence_color} {event_str}" return f"""**`{event_type}`**\n```yaml\n{event_yaml}```""" - def get_severity_color(self, event): - if event.type == "VULNERABILITY": - severity = event.data.get("severity", "UNKNOWN") - return f"{event.type} ({severity})", event.severity_colors[severity] + def get_colors(self, event): + if event.type == "FINDING": + severity = event.data.get("severity", "INFO") + confidence = event.data.get("confidence", "UNKNOWN") + severity_color = event.severity_colors.get(severity, "⬜") + confidence_color = event.confidence_colors.get(confidence, "⚪") + return f"{event.type} (Severity: {severity} / Confidence: {confidence})", severity_color, confidence_color else: - return event.type, "🟦" + return event.type, "🟦", "" def format_message(self, event): if isinstance(event.data, str): diff --git a/bbot/modules/trajan.py b/bbot/modules/trajan.py new file mode 100644 index 0000000000..dec96d94fa --- /dev/null +++ b/bbot/modules/trajan.py @@ -0,0 +1,280 @@ +import json +from urllib.parse import urlparse + +from bbot.modules.base import BaseModule + + +class trajan(BaseModule): + watched_events = ["CODE_REPOSITORY", "URL_UNVERIFIED", "TECHNOLOGY"] + produced_events = ["FINDING"] + flags = ["safe", "passive", "code-enum"] + meta = { + "description": "Scans GitHub, GitLab, Azure DevOps, Jenkins, and JFrog for misconfigurations using Praetorian's Trajan tool", + "created_date": "2026-04-11", + "author": "@N7WERA", + } + + # Configuration options + options = { + "version": "1.0.0", + "github_token": "", + "gitlab_token": "", + "ado_token": "", + "jfrog_token": "", + "jenkins_username": "", + "jenkins_password": "", + "jenkins_token": "", + } + options_desc = { + "version": "Trajan version to download and use", + "github_token": "GitHub API token for rate-limiting and private repo access", + "gitlab_token": "GitLab API token for private repo access", + "ado_token": "Azure DevOps Personal Access Token (PAT)", + "jfrog_token": "JFrog API token", + "jenkins_username": "Jenkins username for basic auth", + "jenkins_password": "Jenkins password for basic auth", + "jenkins_token": "Jenkins API token", + } + + deps_ansible = [ + { + "name": "Download Trajan binary", + "unarchive": { + "src": "https://github.com/praetorian-inc/trajan/releases/download/v#{BBOT_MODULES_TRAJAN_VERSION}/trajan_#{BBOT_MODULES_TRAJAN_VERSION}_#{BBOT_OS}_#{BBOT_CPU_ARCH_GOLANG}.tar.gz", + "include": "trajan", + "dest": "#{BBOT_TOOLS}", + "remote_src": True, + }, + } + ] + + # Platform detection from URL domains (SaaS platforms) + domain_platforms = { + "github.com": "github", + "gitlab.com": "gitlab", + "jfrog.com": "jfrog", + "jfrog.io": "jfrog", + } + + # Platform detection from TECHNOLOGY event names (lowercased) + technology_platforms = { + "jenkins": "jenkins", + "jfrog": "jfrog", + "artifactory": "jfrog", + } + + async def setup(self): + self.github_token = self.config.get("github_token", "") + + # Borrow GitHub token from other modules if not explicitly set + if not self.github_token: + for module_name in ("github", "github_codesearch", "github_org", "git_clone"): + other_config = self.scan.config.get("modules", {}).get(module_name, {}) + api_key = other_config.get("api_key", "") + if api_key: + self.github_token = api_key + self.debug(f"Borrowing GitHub token from {module_name}") + break + + self.gitlab_token = self.config.get("gitlab_token", "") + self.ado_token = self.config.get("ado_token", "") + self.jfrog_token = self.config.get("jfrog_token", "") + self.jenkins_username = self.config.get("jenkins_username", "") + self.jenkins_password = self.config.get("jenkins_password", "") + self.jenkins_token = self.config.get("jenkins_token", "") + if self.jenkins_token or (self.jenkins_username and self.jenkins_password): + self.warning( + "Jenkins credentials are configured. These will be sent to ANY in-scope server detected as Jenkins!" + ) + return True + + def detect_platform_from_url(self, hostname, domain): + """Detect platform from URL hostname/domain.""" + if domain == "azure.com" and hostname.startswith("dev."): + return "ado" + return self.domain_platforms.get(domain) + + def detect_platform_from_technology(self, technology): + """Detect platform from a TECHNOLOGY event's technology name.""" + return self.technology_platforms.get(technology.lower()) + + def _get_platform(self, event): + """Detect the platform for an event, or None if not applicable.""" + if event.type == "TECHNOLOGY": + tech = event.data.get("technology", "").lower() + return self.detect_platform_from_technology(tech) + hostname = str(event.host) + _, domain = self.helpers.split_domain(hostname) + return self.detect_platform_from_url(hostname, domain) + + def _incoming_dedup_hash(self, event): + platform = self._get_platform(event) + return hash(f"{platform}:{event.host}"), f"already scanned {event.host} as {platform}" + + async def filter_event(self, event): + if event.type == "TECHNOLOGY": + tech = event.data.get("technology", "").lower() + if tech not in self.technology_platforms: + return False, f"technology '{tech}' is not supported by trajan" + if not event.url: + return False, "TECHNOLOGY event has no URL" + + platform = self._get_platform(event) + if platform is None: + return False, "could not determine platform from event" + return True + + async def handle_event(self, event): + if event.type == "TECHNOLOGY": + await self.handle_technology(event) + else: + await self.handle_url(event) + + async def handle_technology(self, event): + tech = event.data.get("technology", "").lower() + platform = self.detect_platform_from_technology(tech) + url = event.url + parsed = urlparse(url) + base_url = f"{parsed.scheme}://{parsed.netloc}" + path_parts = [p for p in parsed.path.strip("/").split("/") if p] + + handler = getattr(self, f"handle_{platform}", None) + if handler: + await handler(event, parsed, base_url, path_parts) + + async def handle_url(self, event): + parsed = getattr(event, "parsed_url", None) + hostname = parsed.hostname + _, domain = self.helpers.split_domain(hostname) + platform = self.detect_platform_from_url(hostname, domain) + base_url = f"{parsed.scheme}://{parsed.netloc}" + path_parts = [p for p in parsed.path.strip("/").split("/") if p] + + handler = getattr(self, f"handle_{platform}", None) + if handler: + await handler(event, parsed, base_url, path_parts) + + async def handle_github(self, event, parsed, base_url, path_parts): + if not self.github_token: + self.warning(f"Skipping {base_url} - GitHub token required for Trajan") + return + if len(path_parts) < 2: + return + repo_path = f"{path_parts[0]}/{path_parts[1]}" + command = ["trajan", "github", "scan", "--repo", repo_path, "-o", "json", "--token", self.github_token] + self.verbose(f"Scanning GitHub {repo_path} with Trajan") + await self.execute_trajan(command, event) + + async def handle_gitlab(self, event, parsed, base_url, path_parts): + if not self.gitlab_token: + self.warning(f"Skipping {base_url} - GitLab token required for Trajan") + return + if len(path_parts) < 2: + return + repo_path = f"{path_parts[0]}/{path_parts[1]}" + command = ["trajan", "gitlab", "scan", "--project", repo_path, "-o", "json", "--token", self.gitlab_token] + self.verbose(f"Scanning GitLab {repo_path} with Trajan") + await self.execute_trajan(command, event) + + async def handle_ado(self, event, parsed, base_url, path_parts): + if not self.ado_token: + self.warning(f"Skipping {base_url} - Azure DevOps token required for Trajan") + return + # e.g., https://dev.azure.com/org/project/_git/repo + if len(path_parts) < 4 or path_parts[2] != "_git": + return + org = path_parts[0] + project = path_parts[1] + repo = path_parts[3] + repo_path = f"{project}/{repo}" + command = ["trajan", "ado", "scan", "--org", org, "--repo", repo_path, "-o", "json", "--token", self.ado_token] + self.verbose(f"Scanning Azure DevOps {org}/{repo_path} with Trajan") + await self.execute_trajan(command, event) + + async def handle_jfrog(self, event, parsed, base_url, path_parts): + if not self.jfrog_token: + self.warning(f"Skipping {base_url} - JFrog token required for Trajan") + return + command = [ + "trajan", + "jfrog", + "scan", + "--url", + base_url, + "--secrets", + "-o", + "json", + "--token", + self.jfrog_token, + ] + self.verbose(f"Scanning JFrog {base_url} with Trajan") + await self.execute_trajan(command, event, is_jfrog=True) + + async def handle_jenkins(self, event, parsed, base_url, path_parts): + if not self.jenkins_token and not (self.jenkins_username and self.jenkins_password): + self.warning(f"Skipping {base_url} - Jenkins token or username/password required for Trajan") + return + command = ["trajan", "jenkins", "scan", "--url", base_url, "-o", "json"] + # Check if it's a specific job + try: + job_idx = path_parts.index("job") + if job_idx + 1 < len(path_parts): + command.extend(["--repo", path_parts[job_idx + 1]]) + except ValueError: + pass + if self.jenkins_token: + command.extend(["--token", self.jenkins_token]) + if self.jenkins_username and self.jenkins_password: + command.extend(["--username", self.jenkins_username, "--password", self.jenkins_password]) + self.verbose(f"Scanning Jenkins {base_url} with Trajan") + await self.execute_trajan(command, event) + + async def execute_trajan(self, command, event, is_jfrog=False): + process = await self.run_process(command) + if not process or not process.stdout: + return + + try: + result = json.loads(process.stdout) + except json.JSONDecodeError: + self.debug(f"Trajan JSONDecodeError. Raw stdout: {process.stdout}") + return + + if is_jfrog: + await self.parse_jfrog_results(result, event) + else: + await self.parse_findings(result, event) + + async def parse_findings(self, result, event): + for finding_data in result.get("findings", []): + finding = { + "name": f"Trajan - {finding_data.get('type', 'unknown')}", + "description": finding_data.get("evidence", "No description provided."), + "severity": finding_data.get("severity", "info").upper(), + "confidence": "MEDIUM", + "host": event.host, + } + workflow = finding_data.get("workflow", "") + if workflow: + finding["description"] += f" (Workflow: {workflow})" + await self.emit_event(finding, "FINDING", event) + + async def parse_jfrog_results(self, result, event): + for secret in result.get("artifactSecrets", []): + finding = { + "name": f"Trajan - JFrog Artifact Secret ({', '.join(secret.get('secretTypes', []))})", + "description": f"Secret found in artifact {secret.get('artifact', 'unknown')} at path {secret.get('path', 'unknown')}", + "severity": "HIGH", + "confidence": "HIGH", + "host": event.host, + } + await self.emit_event(finding, "FINDING", event) + for secret in result.get("buildSecrets", []): + finding = { + "name": f"Trajan - JFrog Build Secret ({', '.join(secret.get('secretTypes', []))})", + "description": f"Secret found in build {secret.get('buildName', 'unknown')} #{secret.get('buildNumber', '?')} env var {secret.get('envVar', 'unknown')}", + "severity": "HIGH", + "confidence": "HIGH", + "host": event.host, + } + await self.emit_event(finding, "FINDING", event) diff --git a/bbot/modules/trickest.py b/bbot/modules/trickest.py index 246fdcfdec..7cbbfda3c4 100644 --- a/bbot/modules/trickest.py +++ b/bbot/modules/trickest.py @@ -4,7 +4,7 @@ class Trickest(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["affiliates", "subdomain-enum", "passive", "safe"] + flags = ["safe", "affiliates", "subdomain-enum", "passive"] meta = { "description": "Query Trickest's API for subdomains", "author": "@amiremami", diff --git a/bbot/modules/trufflehog.py b/bbot/modules/trufflehog.py index 27347fa6ba..21a233262a 100644 --- a/bbot/modules/trufflehog.py +++ b/bbot/modules/trufflehog.py @@ -5,8 +5,8 @@ class trufflehog(BaseModule): watched_events = ["CODE_REPOSITORY", "FILESYSTEM", "HTTP_RESPONSE", "RAW_TEXT"] - produced_events = ["FINDING", "VULNERABILITY"] - flags = ["passive", "safe", "code-enum"] + produced_events = ["FINDING"] + flags = ["safe", "passive", "code-enum"] meta = { "description": "TruffleHog is a tool for finding credentials", "created_date": "2024-03-12", @@ -75,7 +75,7 @@ async def filter_event(self, event): if self.deleted_forks: if "git" not in event.tags: return False, "Module only accepts git CODE_REPOSITORY events" - if "github" not in event.data["url"]: + if "github" not in event.url: return False, "Module only accepts github CODE_REPOSITORY events" else: return False, "Deleted forks is not enabled" @@ -90,7 +90,7 @@ async def handle_event(self, event): description = event.data.get("description", "") if event.type == "CODE_REPOSITORY": - path = event.data["url"] + path = event.url module = "github-experimental" elif event.type == "FILESYSTEM": path = event.data["path"] @@ -123,14 +123,16 @@ async def handle_event(self, event): source_metadata, ) in self.execute_trufflehog(module, path): verified_str = "Verified" if verified else "Possible" - finding_type = "VULNERABILITY" if verified else "FINDING" + confidence = "CONFIRMED" if verified else "MEDIUM" data = { + "name": f"TruffleHog - {detector_name}", "description": f"{verified_str} Secret Found. Detector Type: [{detector_name}] Decoder Type: [{decoder_name}] Details: [{source_metadata}]", } if host: data["host"] = host - if finding_type == "VULNERABILITY": - data["severity"] = "High" + + data["severity"] = "HIGH" + data["confidence"] = confidence if description: data["description"] += f" Description: [{description}]" data["description"] += f" Raw result: [{raw_result}]" @@ -138,7 +140,7 @@ async def handle_event(self, event): data["description"] += f" RawV2 result: [{rawv2_result}]" await self.emit_event( data, - finding_type, + "FINDING", event, context=f'{{module}} searched {event.type} using "{module}" method and found {verified_str.lower()} secret ({{event.type}}): {raw_result}', ) diff --git a/bbot/modules/url_manipulation.py b/bbot/modules/url_manipulation.py index c36b7c39d5..d9b7db466c 100644 --- a/bbot/modules/url_manipulation.py +++ b/bbot/modules/url_manipulation.py @@ -5,7 +5,7 @@ class url_manipulation(BaseModule): watched_events = ["URL"] produced_events = ["FINDING"] - flags = ["active", "aggressive", "web-thorough"] + flags = ["active", "loud", "web-heavy"] meta = { "description": "Attempt to identify URL parsing/routing based vulnerabilities", "created_date": "2022-09-27", @@ -45,17 +45,17 @@ async def setup(self): async def handle_event(self, event): try: compare_helper = self.helpers.http_compare( - event.data, allow_redirects=self.allow_redirects, include_cache_buster=False + event.url, allow_redirects=self.allow_redirects, include_cache_buster=False ) except HttpCompareError as e: self.debug(e) return try: - if not await compare_helper.canary_check(event.data, mode="getparam"): + if not await compare_helper.canary_check(event.url, mode="getparam"): raise HttpCompareError() except HttpCompareError: - self.verbose(f'Aborting "{event.data}" due to failed canary check') + self.verbose(f'Aborting "{event.url}" due to failed canary check') return for sig in self.signatures: @@ -65,7 +65,7 @@ async def handle_event(self, event): sig[1], method=sig[0], allow_redirects=self.allow_redirects ) except HttpCompareError as e: - self.debug(f"Encountered HttpCompareError: [{e}] for URL [{event.data}]") + self.debug(f"Encountered HttpCompareError: [{e}] for URL [{event.url}]") if subject_response: subject_content = "".join([str(x) for x in subject_response.headers]) @@ -77,12 +77,19 @@ async def handle_event(self, event): if str(subject_response.status_code).startswith("2"): if "body" in reasons: reported_signature = f"Modified URL: {sig[1]}" - description = f"Url Manipulation: [{','.join(reasons)}] Sig: [{reported_signature}]" + description = f"URL Manipulation: [{','.join(reasons)}] Sig: [{reported_signature}]" await self.emit_event( - {"description": description, "host": str(event.host), "url": event.data}, + { + "description": description, + "host": str(event.host), + "url": event.url, + "name": "URL Manipulation", + "severity": "INFO", + "confidence": "LOW", + }, "FINDING", parent=event, - context=f"{{module}} probed {event.data} and identified {{event.type}}: {description}", + context=f"{{module}} probed {event.url} and identified {{event.type}}: {description}", ) else: self.debug(f"Status code changed to {str(subject_response.status_code)}, ignoring") diff --git a/bbot/modules/urlscan.py b/bbot/modules/urlscan.py index 5c7c78f478..9a09222fe2 100644 --- a/bbot/modules/urlscan.py +++ b/bbot/modules/urlscan.py @@ -2,7 +2,7 @@ class urlscan(subdomain_enum): - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME", "URL_UNVERIFIED"] meta = { @@ -30,7 +30,7 @@ async def handle_event(self, event): await self.emit_event( domain_event, abort_if=self.abort_if, - context=f'{{module}} searched urlscan.io API for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched urlscan.io API for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) parent_event = domain_event if url: @@ -41,7 +41,7 @@ async def handle_event(self, event): await self.emit_event( url_event, abort_if=self.abort_if, - context=f'{{module}} searched urlscan.io API for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched urlscan.io API for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) else: await self.emit_event( @@ -49,7 +49,7 @@ async def handle_event(self, event): "DNS_NAME", parent=event, abort_if=self.abort_if, - context=f'{{module}} searched urlscan.io API for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched urlscan.io API for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) else: self.debug(f"{url_event.host} does not match {query}") diff --git a/bbot/modules/vhost.py b/bbot/modules/vhost.py deleted file mode 100644 index 0c8759f097..0000000000 --- a/bbot/modules/vhost.py +++ /dev/null @@ -1,129 +0,0 @@ -import base64 -from urllib.parse import urlparse - -from bbot.modules.ffuf import ffuf - - -class vhost(ffuf): - watched_events = ["URL"] - produced_events = ["VHOST", "DNS_NAME"] - flags = ["active", "aggressive", "slow", "deadly"] - meta = {"description": "Fuzz for virtual hosts", "created_date": "2022-05-02", "author": "@liquidsec"} - - special_vhost_list = ["127.0.0.1", "localhost", "host.docker.internal"] - options = { - "wordlist": "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/subdomains-top1million-5000.txt", - "force_basehost": "", - "lines": 5000, - } - options_desc = { - "wordlist": "Wordlist containing subdomains", - "force_basehost": "Use a custom base host (e.g. evilcorp.com) instead of the default behavior of using the current URL", - "lines": "take only the first N lines from the wordlist when finding directories", - } - - deps_common = ["ffuf"] - banned_characters = {" ", "."} - - in_scope_only = True - - async def setup(self): - self.scanned_hosts = {} - self.wordcloud_tried_hosts = set() - return await super().setup() - - async def handle_event(self, event): - if not self.helpers.is_ip(event.host) or self.config.get("force_basehost"): - host = f"{event.parsed_url.scheme}://{event.parsed_url.netloc}" - if host in self.scanned_hosts.keys(): - return - else: - self.scanned_hosts[host] = event - - # subdomain vhost check - self.verbose("Main vhost bruteforce") - if self.config.get("force_basehost"): - basehost = self.config.get("force_basehost") - else: - basehost = self.helpers.parent_domain(event.parsed_url.netloc) - - self.debug(f"Using basehost: {basehost}") - async for vhost in self.ffuf_vhost(host, f".{basehost}", event): - self.verbose(f"Starting mutations check for {vhost}") - async for vhost in self.ffuf_vhost(host, f".{basehost}", event, wordlist=self.mutations_check(vhost)): - pass - - # check existing host for mutations - self.verbose("Checking for vhost mutations on main host") - async for vhost in self.ffuf_vhost( - host, f".{basehost}", event, wordlist=self.mutations_check(event.parsed_url.netloc.split(".")[0]) - ): - pass - - # special vhost list - self.verbose("Checking special vhost list") - async for vhost in self.ffuf_vhost( - host, - "", - event, - wordlist=self.helpers.tempfile(self.special_vhost_list, pipe=False), - skip_dns_host=True, - ): - pass - - async def ffuf_vhost(self, host, basehost, event, wordlist=None, skip_dns_host=False): - filters = await self.baseline_ffuf(f"{host}/", exts=[""], suffix=basehost, mode="hostheader") - self.debug("Baseline completed and returned these filters:") - self.debug(filters) - if not wordlist: - wordlist = self.tempfile - async for r in self.execute_ffuf( - wordlist, host, exts=[""], suffix=basehost, filters=filters, mode="hostheader" - ): - found_vhost_b64 = r["input"]["FUZZ"] - vhost_str = base64.b64decode(found_vhost_b64).decode() - vhost_dict = {"host": str(event.host), "url": host, "vhost": vhost_str} - if f"{vhost_dict['vhost']}{basehost}" != event.parsed_url.netloc: - await self.emit_event( - vhost_dict, - "VHOST", - parent=event, - context=f"{{module}} brute-forced virtual hosts for {event.data} and found {{event.type}}: {vhost_str}", - ) - if skip_dns_host is False: - await self.emit_event( - f"{vhost_dict['vhost']}{basehost}", - "DNS_NAME", - parent=event, - tags=["vhost"], - context=f"{{module}} brute-forced virtual hosts for {event.data} and found {{event.type}}: {{event.data}}", - ) - - yield vhost_dict["vhost"] - - def mutations_check(self, vhost): - mutations_list = [] - for mutation in self.helpers.word_cloud.mutations(vhost): - for i in ["", "-"]: - mutations_list.append(i.join(mutation)) - mutations_list_file = self.helpers.tempfile(mutations_list, pipe=False) - return mutations_list_file - - async def finish(self): - # check existing hosts with wordcloud - tempfile = self.helpers.tempfile(list(self.helpers.word_cloud.keys()), pipe=False) - - for host, event in self.scanned_hosts.items(): - if host not in self.wordcloud_tried_hosts: - event.parsed_url = urlparse(host) - - self.verbose("Checking main host with wordcloud") - if self.config.get("force_basehost"): - basehost = self.config.get("force_basehost") - else: - basehost = self.helpers.parent_domain(event.parsed_url.netloc) - - async for vhost in self.ffuf_vhost(host, f".{basehost}", event, wordlist=tempfile): - pass - - self.wordcloud_tried_hosts.add(host) diff --git a/bbot/modules/viewdns.py b/bbot/modules/viewdns.py index 7a60b721ba..89466ffea3 100644 --- a/bbot/modules/viewdns.py +++ b/bbot/modules/viewdns.py @@ -10,7 +10,7 @@ class viewdns(BaseModule): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["affiliates", "passive", "safe"] + flags = ["safe", "affiliates", "passive"] meta = { "description": "Query viewdns.info's reverse whois for related domains", "created_date": "2022-07-04", @@ -33,7 +33,7 @@ async def handle_event(self, event): "DNS_NAME", parent=event, tags=["affiliate"], - context=f'{{module}} searched viewdns.info for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} searched viewdns.info for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) async def query(self, query): diff --git a/bbot/modules/virustotal.py b/bbot/modules/virustotal.py index b932419450..75766b38d8 100644 --- a/bbot/modules/virustotal.py +++ b/bbot/modules/virustotal.py @@ -4,7 +4,7 @@ class virustotal(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] - flags = ["subdomain-enum", "passive", "safe"] + flags = ["safe", "subdomain-enum", "passive"] meta = { "description": "Query VirusTotal's API for subdomains", "created_date": "2022-08-25", diff --git a/bbot/modules/wafw00f.py b/bbot/modules/wafw00f.py index aa6f14df8c..6063a3be33 100644 --- a/bbot/modules/wafw00f.py +++ b/bbot/modules/wafw00f.py @@ -15,7 +15,7 @@ class wafw00f(BaseModule): watched_events = ["URL"] produced_events = ["WAF"] - flags = ["active", "aggressive"] + flags = ["active", "loud"] meta = { "description": "Web Application Firewall Fingerprinting Tool", "created_date": "2023-02-15", diff --git a/bbot/modules/wayback.py b/bbot/modules/wayback.py index fbb30da7a9..49010f451a 100644 --- a/bbot/modules/wayback.py +++ b/bbot/modules/wayback.py @@ -4,7 +4,7 @@ class wayback(subdomain_enum): - flags = ["passive", "subdomain-enum", "safe"] + flags = ["safe", "passive", "subdomain-enum"] watched_events = ["DNS_NAME"] produced_events = ["URL_UNVERIFIED", "DNS_NAME"] meta = { @@ -34,7 +34,7 @@ async def handle_event(self, event): event_type, event, abort_if=self.abort_if, - context=f'{{module}} queried archive.org for "{query}" and found {{event.type}}: {{event.data}}', + context=f'{{module}} queried archive.org for "{query}" and found {{event.type}}: {{event.pretty_string}}', ) async def query(self, query): diff --git a/bbot/modules/wpscan.py b/bbot/modules/wpscan.py index 4f1a63a1b5..b7a703b90d 100644 --- a/bbot/modules/wpscan.py +++ b/bbot/modules/wpscan.py @@ -4,8 +4,8 @@ class wpscan(BaseModule): watched_events = ["HTTP_RESPONSE", "TECHNOLOGY"] - produced_events = ["URL_UNVERIFIED", "FINDING", "VULNERABILITY", "TECHNOLOGY"] - flags = ["active", "aggressive"] + produced_events = ["URL_UNVERIFIED", "FINDING", "TECHNOLOGY"] + flags = ["active", "loud"] meta = { "description": "Wordpress security scanner. Highly recommended to use an API key for better results.", "created_date": "2024-05-29", @@ -174,7 +174,14 @@ def parse_wp_misc(self, interesting_json, base_url, source_event): if url_event: yield url_event yield self.make_event( - {"description": description_string, "url": url, "host": str(source_event.host)}, + { + "description": description_string, + "url": url, + "host": str(source_event.host), + "name": "WPScan - Possible Vulnerability", + "severity": "INFO", + "confidence": "MEDIUM", + }, "FINDING", source_event, ) @@ -194,11 +201,13 @@ def parse_wp_version(self, version_json, url, source_event): yield self.make_event( { "severity": "HIGH", + "confidence": "MEDIUM", "host": str(source_event.host), "url": url, "description": self.vulnerability_to_s(wp_vuln), + "name": "WPScan - Possible Vulnerability", }, - "VULNERABILITY", + "FINDING", source_event, ) @@ -219,11 +228,13 @@ def parse_wp_themes(self, theme_json, url, source_event): yield self.make_event( { "severity": "HIGH", + "confidence": "MEDIUM", "host": str(source_event.host), "url": url, "description": self.vulnerability_to_s(theme_vuln), + "name": "WPScan - Possible Vulnerability", }, - "VULNERABILITY", + "FINDING", source_event, ) @@ -248,11 +259,13 @@ def parse_wp_plugins(self, plugins_json, base_url, source_event): yield self.make_event( { "severity": "HIGH", + "confidence": "MEDIUM", "host": str(source_event.host), "url": url, "description": self.vulnerability_to_s(vuln), + "name": "WPScan - Possible Vulnerability", }, - "VULNERABILITY", + "FINDING", source_event, ) @@ -279,7 +292,7 @@ def vulnerability_to_s(self, vuln_json): return " ".join(string) def get_base_url(self, event): - base_url = event.data.get("url", "") + base_url = event.url if not base_url: base_url = f"https://{event.host}" return self.helpers.urlparse(base_url)._replace(path="/").geturl() diff --git a/bbot/presets/baddns-heavy.yml b/bbot/presets/baddns-heavy.yml new file mode 100644 index 0000000000..bb683a9d0a --- /dev/null +++ b/bbot/presets/baddns-heavy.yml @@ -0,0 +1,21 @@ +description: Run all baddns modules and submodules. + +include: + - baddns + +modules: + - baddns_zone + - baddns_direct + +config: + modules: + baddns: + enabled_submodules: [CNAME, NS, MX, TXT, references, DMARC, SPF, MTA-STS, WILDCARD] + min_severity: INFO + min_confidence: UNKNOWN + baddns_zone: + min_severity: INFO + min_confidence: UNKNOWN + baddns_direct: + min_severity: INFO + min_confidence: UNKNOWN diff --git a/bbot/presets/baddns-intense.yml b/bbot/presets/baddns-intense.yml deleted file mode 100644 index 8afeebd3d9..0000000000 --- a/bbot/presets/baddns-intense.yml +++ /dev/null @@ -1,12 +0,0 @@ -description: Run all baddns modules and submodules. - - -modules: - - baddns - - baddns_zone - - baddns_direct - -config: - modules: - baddns: - enabled_submodules: [CNAME,references,MX,NS,TXT] diff --git a/bbot/presets/baddns.yml b/bbot/presets/baddns.yml new file mode 100644 index 0000000000..c737257011 --- /dev/null +++ b/bbot/presets/baddns.yml @@ -0,0 +1,11 @@ +description: Check for subdomain takeovers and other DNS issues. + +modules: + - baddns + +config: + modules: + baddns: + enabled_submodules: [CNAME, MX, TXT] + min_severity: LOW + min_confidence: MODERATE diff --git a/bbot/presets/kitchen-sink.yml b/bbot/presets/kitchen-sink.yml index c01039099e..647889aab5 100644 --- a/bbot/presets/kitchen-sink.yml +++ b/bbot/presets/kitchen-sink.yml @@ -6,13 +6,8 @@ include: - code-enum - email-enum - spider - - web-basic + - web - paramminer - dirbust-light - web-screenshots - - baddns-intense - -config: - modules: - baddns: - enable_references: True + - baddns-heavy diff --git a/bbot/presets/nuclei/nuclei-intense.yml b/bbot/presets/nuclei/nuclei-heavy.yml similarity index 70% rename from bbot/presets/nuclei/nuclei-intense.yml rename to bbot/presets/nuclei/nuclei-heavy.yml index 27f833c387..6468ba9c81 100644 --- a/bbot/presets/nuclei/nuclei-intense.yml +++ b/bbot/presets/nuclei/nuclei-heavy.yml @@ -18,11 +18,11 @@ config: conditions: - | {% if config.web.spider_distance == 0 and config.modules.nuclei.directory_only == False %} - {{ warn("The 'nuclei-intense' preset turns the 'directory_only' limitation off on the nuclei module. To make the best use of this, you may want to enable spidering with 'spider' or 'spider-intense' preset.") }} + {{ warn("The 'nuclei-heavy' preset turns the 'directory_only' limitation off on the nuclei module. To make the best use of this, you may want to enable spidering with 'spider' or 'spider-heavy' preset.") }} {% endif %} # Example for also running a dirbust #include: -# - dirbust-light \ No newline at end of file +# - dirbust-light diff --git a/bbot/presets/spider-intense.yml b/bbot/presets/spider-heavy.yml similarity index 100% rename from bbot/presets/spider-intense.yml rename to bbot/presets/spider-heavy.yml diff --git a/bbot/presets/web-heavy.yml b/bbot/presets/web-heavy.yml new file mode 100644 index 0000000000..4918aeff58 --- /dev/null +++ b/bbot/presets/web-heavy.yml @@ -0,0 +1,8 @@ +description: Aggressive web scan + +include: + # include the web preset + - web + +flags: + - web-heavy diff --git a/bbot/presets/web-thorough.yml b/bbot/presets/web-thorough.yml deleted file mode 100644 index 0294614f76..0000000000 --- a/bbot/presets/web-thorough.yml +++ /dev/null @@ -1,8 +0,0 @@ -description: Aggressive web scan - -include: - # include the web-basic preset - - web-basic - -flags: - - web-thorough diff --git a/bbot/presets/web-basic.yml b/bbot/presets/web.yml similarity index 82% rename from bbot/presets/web-basic.yml rename to bbot/presets/web.yml index 166d973e94..0341ba036a 100644 --- a/bbot/presets/web-basic.yml +++ b/bbot/presets/web.yml @@ -4,4 +4,4 @@ include: - iis-shortnames flags: - - web-basic + - web diff --git a/bbot/presets/web/lightfuzz-heavy.yml b/bbot/presets/web/lightfuzz-heavy.yml index ba2d7ff8f6..ecc8c82c04 100644 --- a/bbot/presets/web/lightfuzz-heavy.yml +++ b/bbot/presets/web/lightfuzz-heavy.yml @@ -1,7 +1,7 @@ -description: Discover web parameters and lightly fuzz them for vulnerabilities, with more intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, and adds paramminer modules for parameter discovery. Avoids running against confirmed WAFs. +description: "Aggressive fuzzing: everything in lightfuzz, plus paramminer brute-force parameter discovery (headers, GET params, cookies), POST request fuzzing enabled, try_get_as_post enabled (GET params retested as POST), and robots.txt parsing. Still skips confirmed WAFs." include: - - lightfuzz-medium + - lightfuzz flags: - web-paramminer @@ -12,7 +12,7 @@ modules: config: modules: lightfuzz: - enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi] + enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] disable_post: False try_post_as_get: True try_get_as_post: True diff --git a/bbot/presets/web/lightfuzz-light.yml b/bbot/presets/web/lightfuzz-light.yml index 85bd2a81a1..052a774cd9 100644 --- a/bbot/presets/web/lightfuzz-light.yml +++ b/bbot/presets/web/lightfuzz-light.yml @@ -1,4 +1,4 @@ -description: Discover web parameters and lightly fuzz them for vulnerabilities, with only the most common vulnerabilities and minimal extra modules. Safest to run alongside larger scans. +description: "Minimal fuzzing: only path traversal, SQLi, and XSS submodules. No POST requests. No companion modules. Safest option for running alongside larger scans with minimal overhead." modules: - httpx @@ -17,5 +17,5 @@ config: conditions: - | {% if config.web.spider_distance == 0 %} - {{ warn("Lightfuzz works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }} + {{ warn("Lightfuzz works much better with spider enabled! Consider adding 'spider' or 'spider-heavy' preset.") }} {% endif %} diff --git a/bbot/presets/web/lightfuzz-superheavy.yml b/bbot/presets/web/lightfuzz-max.yml similarity index 60% rename from bbot/presets/web/lightfuzz-superheavy.yml rename to bbot/presets/web/lightfuzz-max.yml index f05389d2c1..5ad33b817c 100644 --- a/bbot/presets/web/lightfuzz-superheavy.yml +++ b/bbot/presets/web/lightfuzz-max.yml @@ -1,4 +1,4 @@ -description: Discover web parameters and lightly fuzz them for vulnerabilities, with the most intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, adds paramminer modules for parameter discovery, and tests each unique parameter-value instance individually. +description: "Maximum fuzzing: everything in lightfuzz-heavy, plus WAF targets are no longer skipped, each unique parameter-value pair is fuzzed individually (no collapsing), common headers like X-Forwarded-For are fuzzed even if not observed, and potential parameters are speculated from JSON/XML response bodies. Significantly increases scan time." include: - lightfuzz-heavy @@ -8,7 +8,7 @@ config: modules: lightfuzz: force_common_headers: True # Fuzz common headers like X-Forwarded-For even if they're not observed on the target - enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi] + enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] avoid_wafs: False excavate: speculate_params: True # speculate potential parameters extracted from JSON/XML web responses diff --git a/bbot/presets/web/lightfuzz-medium.yml b/bbot/presets/web/lightfuzz-medium.yml deleted file mode 100644 index df714ee722..0000000000 --- a/bbot/presets/web/lightfuzz-medium.yml +++ /dev/null @@ -1,15 +0,0 @@ -description: Discover web parameters and lightly fuzz them for vulnerabilities. Uses all lightfuzz modules, without some of the more intense discovery techniques. Does not send POST requests. This is the default lightfuzz preset; if you're not sure which one to use, this is a good starting point. Avoids running against confirmed WAFs. - -include: - - lightfuzz-light - -modules: - - badsecrets - - hunt - - reflected_parameters - -config: - modules: - lightfuzz: - enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi] - try_post_as_get: True diff --git a/bbot/presets/web/lightfuzz-xss.yml b/bbot/presets/web/lightfuzz-xss.yml index 83354cd291..554f1f2b60 100644 --- a/bbot/presets/web/lightfuzz-xss.yml +++ b/bbot/presets/web/lightfuzz-xss.yml @@ -1,4 +1,4 @@ -description: Discover web parameters and lightly fuzz them, limited to just GET-based xss vulnerabilities. Avoids running against confirmed WAFs. This is an example of a custom lightfuzz preset, selectively enabling a single lightfuzz module. +description: "XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST disabled, no query string collapsing. Example of a focused single-submodule preset." modules: - httpx @@ -18,5 +18,5 @@ config: conditions: - | {% if config.web.spider_distance == 0 %} - {{ warn("The lightfuzz-xss preset works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }} + {{ warn("The lightfuzz-xss preset works much better with spider enabled! Consider adding 'spider' or 'spider-heavy' preset.") }} {% endif %} diff --git a/bbot/presets/web/lightfuzz.yml b/bbot/presets/web/lightfuzz.yml new file mode 100644 index 0000000000..49c4e16567 --- /dev/null +++ b/bbot/presets/web/lightfuzz.yml @@ -0,0 +1,15 @@ +description: "Default fuzzing: all 9 submodules (cmdi, crypto, path, serial, sqli, ssti, xss, esi, ssrf) plus companion modules (badsecrets, hunt, reflected_parameters). POST fuzzing disabled but try_post_as_get enabled, so POST params are retested as GET. Skips confirmed WAFs." + +include: + - lightfuzz-light + +modules: + - badsecrets + - hunt + - reflected_parameters + +config: + modules: + lightfuzz: + enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] + try_post_as_get: True diff --git a/bbot/presets/web/paramminer.yml b/bbot/presets/web/paramminer.yml index 3057a1b3a9..634e883742 100644 --- a/bbot/presets/web/paramminer.yml +++ b/bbot/presets/web/paramminer.yml @@ -11,5 +11,5 @@ modules: conditions: - | {% if config.web.spider_distance == 0 %} - {{ warn("The paramminer preset works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }} + {{ warn("The paramminer preset works much better with spider enabled! Consider adding 'spider' or 'spider-heavy' preset.") }} {% endif %} \ No newline at end of file diff --git a/bbot/scanner/dispatcher.py b/bbot/scanner/dispatcher.py index a9c56c2b72..efd3270903 100644 --- a/bbot/scanner/dispatcher.py +++ b/bbot/scanner/dispatcher.py @@ -1,7 +1,6 @@ import logging import traceback - -log = logging.getLogger("bbot.scanner.dispatcher") +import contextlib class Dispatcher: @@ -11,6 +10,7 @@ class Dispatcher: def set_scan(self, scan): self.scan = scan + self.log = logging.getLogger("bbot.scanner.dispatcher") async def on_start(self, scan): return @@ -24,9 +24,10 @@ async def on_status(self, status, scan_id): """ self.scan.debug(f"Setting scan status to {status}") - async def catch(self, callback, *args, **kwargs): + @contextlib.contextmanager + def catch(self): try: - return await callback(*args, **kwargs) + yield except Exception as e: - log.error(f"Error in {callback.__qualname__}(): {e}") - log.trace(traceback.format_exc()) + self.log.error(f"Error in dispatcher: {e}") + self.log.trace(traceback.format_exc()) diff --git a/bbot/scanner/manager.py b/bbot/scanner/manager.py index 14b117ddce..0ad7f59c85 100644 --- a/bbot/scanner/manager.py +++ b/bbot/scanner/manager.py @@ -1,6 +1,6 @@ import asyncio from contextlib import suppress -from radixtarget.helpers import host_size_key +from radixtarget import host_size_key from bbot.modules.base import BaseInterceptModule @@ -48,18 +48,21 @@ async def init_events(self, event_seeds=None): event_seeds = sorted(event_seeds, key=lambda e: (host_size_key(str(e.host)), e.data)) # queue root scan event await self.queue_event(root_event, {}) - target_module = self.scan._make_dummy_module(name="TARGET", _type="TARGET") - # queue each target in turn + target_module = self.scan._make_dummy_module(name="SEED", _type="SEED") + # queue each seed in turn for event_seed in event_seeds: event = self.scan.make_event( event_seed.data, event_seed.type, parent=root_event, module=target_module, - context=f"Scan {self.scan.name} seeded with " + "{event.type}: {event.data}", - tags=["target"], + context=f"Scan {self.scan.name} seeded with " + "{event.type}: {event.pretty_string}", + tags=["seed"], ) - self.verbose(f"Target: {event}") + # If the seed is also in the target scope, add the target tag + if self.scan.in_target(event): + event.add_tag("target") + self.verbose(f"Seed: {event}") # don't fill up the queue with too many events while self.incoming_event_queue.qsize() > 100: await asyncio.sleep(0.2) @@ -113,9 +116,9 @@ async def handle_event(self, event, **kwargs): # Scope shepherding # here is where we make sure in-scope events are set to their proper scope distance + if event.host: - event_whitelisted = self.scan.whitelisted(event) - if event_whitelisted: + if self.scan.in_target(event): self.debug(f"Making {event} in-scope because its main host matches the scan target") event.scope_distance = 0 @@ -268,3 +271,7 @@ async def forward_event(self, event, kwargs): # don't distribute events to intercept modules if not mod._intercept: await mod.queue_event(event) + + # if no module accepted this event, minimize it now + if event._module_consumers <= 0: + event._minimize() diff --git a/bbot/scanner/preset/args.py b/bbot/scanner/preset/args.py index b018fe395c..f99305e5d8 100644 --- a/bbot/scanner/preset/args.py +++ b/bbot/scanner/preset/args.py @@ -41,7 +41,7 @@ class BBOTArgs: ( "Subdomains + basic web scan", "A basic web scan includes robots.txt, storage buckets, IIS shortnames, and other non-intrusive web modules", - "bbot -t evilcorp.com -p subdomain-enum web-basic", + "bbot -t evilcorp.com -p subdomain-enum web", ), ( "Web spider", @@ -105,9 +105,12 @@ def parsed(self): def preset_from_args(self): # the order here is important # first we make the preset + # -t/--targets becomes target (defines target, what in_target() checks) + # -s/--seeds becomes seeds (drives passive modules), defaults to targets if not specified + seeds = self.parsed.seeds if self.parsed.seeds is not None else self.parsed.targets args_preset = self.preset.__class__( - *self.parsed.targets, - whitelist=self.parsed.whitelist, + *(self.parsed.targets or []), + seeds=seeds if seeds else None, blacklist=self.parsed.blacklist, name="args_preset", ) @@ -185,21 +188,12 @@ def preset_from_args(self): {"modules": {"excavate": {"custom_yara_rules": self.parsed.custom_yara_rules}}} ) - # Check if both user_agent and user_agent_suffix are set. If so combine them and merge into the config - if self.parsed.user_agent and self.parsed.user_agent_suffix: - modified_user_agent = f"{self.parsed.user_agent} {self.parsed.user_agent_suffix}" - args_preset.core.merge_custom({"web": {"user_agent": modified_user_agent}}) - - # If only user_agent_suffix is set, retrieve the existing user_agent from the merged config and append the suffix - elif self.parsed.user_agent_suffix: - existing_user_agent = args_preset.core.config.get("web", {}).get("user_agent", "") - modified_user_agent = f"{existing_user_agent} {self.parsed.user_agent_suffix}" - args_preset.core.merge_custom({"web": {"user_agent": modified_user_agent}}) - - # If only user_agent is set, merge it directly - elif self.parsed.user_agent: + if self.parsed.user_agent: args_preset.core.merge_custom({"web": {"user_agent": self.parsed.user_agent}}) + if self.parsed.user_agent_suffix: + args_preset.core.merge_custom({"web": {"user_agent_suffix": self.parsed.user_agent_suffix}}) + # CLI config options (dot-syntax) for config_arg in self.parsed.config: try: @@ -225,21 +219,19 @@ def create_parser(self, *args, **kwargs): p = argparse.ArgumentParser(*args, **kwargs) target = p.add_argument_group(title="Target") + target.add_argument("-t", "--targets", nargs="+", default=[], help="Target scope", metavar="TARGET") target.add_argument( - "-t", "--targets", nargs="+", default=[], help="Targets to seed the scan", metavar="TARGET" - ) - target.add_argument( - "-w", - "--whitelist", + "-s", + "--seeds", nargs="+", default=None, - help="What's considered in-scope (by default it's the same as --targets)", + help="Define seeds to drive passive modules without being in scope (if not specified, defaults to same as targets)", ) target.add_argument("-b", "--blacklist", nargs="+", default=[], help="Don't touch these things") target.add_argument( "--strict-scope", action="store_true", - help="Don't consider subdomains of target/whitelist to be in-scope", + help="Don't consider subdomains of target to be in-scope - exact matches only", ) presets = p.add_argument_group(title="Presets") presets.add_argument( @@ -298,16 +290,15 @@ def create_parser(self, *args, **kwargs): "--exclude-flags", nargs="+", default=[], - help="Disable modules with these flags. (e.g. -ef aggressive)", + help="Disable modules with these flags. (e.g. -ef loud)", metavar="FLAG", ) - modules.add_argument("--allow-deadly", action="store_true", help="Enable the use of highly aggressive modules") scan = p.add_argument_group(title="Scan") scan.add_argument("-n", "--name", help="Name of scan (default: random)", metavar="SCAN_NAME") scan.add_argument("-v", "--verbose", action="store_true", help="Be more verbose") scan.add_argument("-d", "--debug", action="store_true", help="Enable debugging") - scan.add_argument("-s", "--silent", action="store_true", help="Be quiet") + scan.add_argument("-S", "--silent", action="store_true", help="Be quiet") scan.add_argument( "--force", action="store_true", @@ -368,6 +359,7 @@ def create_parser(self, *args, **kwargs): deps = p.add_argument_group( title="Module dependencies", description="Control how modules install their dependencies" ) + # Behavior flags are mutually exclusive with each other. But need to be able to be combined with --install-all-deps. g2 = deps.add_mutually_exclusive_group() g2.add_argument("--no-deps", action="store_true", help="Don't install module dependencies") g2.add_argument("--force-deps", action="store_true", help="Force install all module dependencies") @@ -375,7 +367,7 @@ def create_parser(self, *args, **kwargs): g2.add_argument( "--ignore-failed-deps", action="store_true", help="Run modules even if they have failed dependencies" ) - g2.add_argument("--install-all-deps", action="store_true", help="Install dependencies for all modules") + deps.add_argument("--install-all-deps", action="store_true", help="Install dependencies for all modules") misc = p.add_argument_group(title="Misc") misc.add_argument("--version", action="store_true", help="show BBOT version and exit") @@ -397,7 +389,9 @@ def create_parser(self, *args, **kwargs): misc.add_argument("--custom-yara-rules", "-cy", help="Add custom yara rules to excavate") misc.add_argument("--user-agent", "-ua", help="Set the user-agent for all HTTP requests") - misc.add_argument("--user-agent-suffix", "-uas", help=argparse.SUPPRESS, metavar="SUFFIX", default=None) + misc.add_argument( + "--user-agent-suffix", "-uas", help="Suffix to append to the user-agent", metavar="SUFFIX", default=None + ) return p def sanitize_args(self): @@ -409,14 +403,14 @@ def sanitize_args(self): self.parsed.exclude_modules = chain_lists(self.parsed.exclude_modules) self.parsed.output_modules = chain_lists(self.parsed.output_modules) self.parsed.targets = chain_lists( - self.parsed.targets, try_files=True, msg="Reading targets from file: {filename}" + self.parsed.targets, try_files=True, msg="Reading targets from file: {filename}", _strip_comments=True ) - if self.parsed.whitelist is not None: - self.parsed.whitelist = chain_lists( - self.parsed.whitelist, try_files=True, msg="Reading whitelist from file: {filename}" + if self.parsed.seeds is not None: + self.parsed.seeds = chain_lists( + self.parsed.seeds, try_files=True, msg="Reading seeds from file: {filename}", _strip_comments=True ) self.parsed.blacklist = chain_lists( - self.parsed.blacklist, try_files=True, msg="Reading blacklist from file: {filename}" + self.parsed.blacklist, try_files=True, msg="Reading blacklist from file: {filename}", _strip_comments=True ) self.parsed.flags = chain_lists(self.parsed.flags) self.parsed.exclude_flags = chain_lists(self.parsed.exclude_flags) diff --git a/bbot/scanner/preset/path.py b/bbot/scanner/preset/path.py index 56b74392f3..287294401d 100644 --- a/bbot/scanner/preset/path.py +++ b/bbot/scanner/preset/path.py @@ -63,6 +63,29 @@ def add_path(self, path): self.paths = [p for p in self.paths if not p.is_relative_to(path)] self.paths.insert(0, path) + def find_file(self, filename): + """Search known preset paths for a file of any type (e.g. target lists). + + For absolute paths, checks directly. For relative paths, searches each + known preset path and its subdirectories (consistent with how ``find()`` + uses rglob for preset YAML files), then falls back to CWD. + + Returns the resolved Path if found, otherwise None. + """ + filename_path = Path(filename).expanduser() + if filename_path.is_absolute(): + resolved = filename_path.resolve() + return resolved if resolved.is_file() else None + for path in self.paths: + for match in path.rglob(str(filename_path)): + if match.is_file(): + return match.resolve() + # fall back to CWD + candidate = filename_path.resolve() + if candidate.is_file(): + return candidate + return None + def __iter__(self): yield from self.paths diff --git a/bbot/scanner/preset/preset.py b/bbot/scanner/preset/preset.py index b81bfda65c..c6ab3a8220 100644 --- a/bbot/scanner/preset/preset.py +++ b/bbot/scanner/preset/preset.py @@ -76,8 +76,8 @@ class Preset(metaclass=BasePreset): Based on the state of the preset, you can print a warning message, abort the scan, enable/disable modules, etc.. Attributes: - target (Target): Target(s) of scan. - whitelist (Target): Scan whitelist (by default this is the same as `target`). + target (BBOTTarget): The scan target object containing seeds, target, and blacklist. + Use `target.target` to access what's in the target (what `in_target()` checks). blacklist (Target): Scan blacklist (this takes ultimate precedence). helpers (ConfigAwareHelper): Helper containing various reusable functions, regexes, etc. output_dir (pathlib.Path): Output directory for scan. @@ -115,8 +115,8 @@ class Preset(metaclass=BasePreset): def __init__( self, - *targets, - whitelist=None, + *target, + seeds=None, blacklist=None, modules=None, output_modules=None, @@ -142,8 +142,11 @@ def __init__( Initializes the Preset class. Args: - *targets (str): Target(s) to scan. Types supported: hostnames, IPs, CIDRs, emails, open ports. - whitelist (list, optional): Whitelisted target(s) to scan. Defaults to the same as `targets`. + *target (str): Target(s) to scan. These ALWAYS become the target (what `in_target()` checks). + Types supported: hostnames, IPs, CIDRs, emails, open ports. + Note: Positional arguments always mean target, never seeds. + seeds (list, optional): Explicitly define seeds (initial events for passive modules). + If not specified, seeds will be backfilled from target when target is defined. blacklist (list, optional): Blacklisted target(s). Takes ultimate precedence. Defaults to empty. modules (list[str], optional): List of scan modules to enable for the scan. Defaults to empty list. output_modules (list[str], optional): List of output modules to use. Defaults to csv, human, and json. @@ -231,7 +234,7 @@ def __init__( # preset description, default blank self.description = description or "" - # custom conditions, evaluated during .bake() + # custom conditions, evaluated during Scanner._prep() self.conditions = [] if conditions is not None: for condition in conditions: @@ -260,12 +263,17 @@ def __init__( self._module_dirs = set() self.module_dirs = module_dirs - # target / whitelist / blacklist + # target / seeds / blacklist # these are temporary receptacles until they all get .baked() together - self._seeds = set(targets if targets else []) - self._whitelist = set(whitelist) if whitelist else whitelist + self._target_list = set(target or []) self._blacklist = set(blacklist if blacklist else []) + # seeds are special. Instead of initializing them as an empty set, we use "None" + # to signify they haven't been explicitly set. + # after all the merging is done, if seeds are still untouched by the user + # (i.e. they are still None), we'll know it's okay to copy them from the targets. + self._seeds = set(seeds) if seeds else None + # _target doesn't get set until .bake() self._target = None # we don't fill self.modules yet (that happens in .bake()) @@ -288,16 +296,10 @@ def target(self): @property def seeds(self): - if self._seeds is None: + if self._target is None: raise ValueError("Cannot access target before preset is baked (use ._seeds instead)") return self.target.seeds - @property - def whitelist(self): - if self._target is None: - raise ValueError("Cannot access whitelist before preset is baked (use ._whitelist instead)") - return self.target.whitelist - @property def blacklist(self): if self._target is None: @@ -364,13 +366,12 @@ def merge(self, other): self.flags.update(other.flags) # target / scope - self._seeds.update(other._seeds) - # leave whitelist as None until we encounter one - if other._whitelist is not None: - if self._whitelist is None: - self._whitelist = set(other._whitelist) + self._target_list.update(other._target_list) + if other._seeds is not None: + if self._seeds is None: + self._seeds = set(other._seeds) else: - self._whitelist.update(other._whitelist) + self._seeds.update(other._seeds) self._blacklist.update(other._blacklist) # module dirs @@ -402,9 +403,10 @@ def bake(self, scan=None): """ Return a "baked" copy of this preset, ready for use by a BBOT scan. + Presets can be merged and modified before baking, but once baked, they are immutable. + Baking a preset finalizes it by populating `preset.modules` based on flags, performing final validations, and substituting environment variables in preloaded modules. - It also evaluates custom `conditions` as specified in the preset. This function is automatically called in Scanner.__init__(). There is no need to call it manually. """ @@ -429,9 +431,6 @@ def bake(self, scan=None): os.environ.clear() os.environ.update(os_environ) - # assign baked preset to our scan - scan.preset = baked_preset - # validate log level options baked_preset.apply_log_level(apply_core=scan is not None) @@ -483,20 +482,12 @@ def bake(self, scan=None): from bbot.scanner.target import BBOTTarget baked_preset._target = BBOTTarget( - *list(self._seeds), - whitelist=self._whitelist, + seeds=list(self._seeds) if self._seeds else None, + target=list(self._target_list), blacklist=self._blacklist, strict_scope=self.strict_scope, ) - if scan is not None: - # evaluate conditions - if baked_preset.conditions: - from .conditions import ConditionEvaluator - - evaluator = ConditionEvaluator(baked_preset) - evaluator.evaluate() - self._baked = True return baked_preset @@ -635,8 +626,25 @@ def in_scope(self, host): def blacklisted(self, host): return self.target.blacklisted(host) - def whitelisted(self, host): - return self.target.whitelisted(host) + def in_target(self, host): + return self.target.in_target(host) + + @staticmethod + def _resolve_file_entries(entries): + """Resolve relative file paths in target/seeds/blacklist entries via PresetPath. + + Replaces entries that match a file in PresetPath's known directories with + their absolute path, so that chain_lists' existing try_files logic can find them. + Entries that don't match a file are left as-is. + """ + resolved = [] + for entry in entries: + found = PRESET_PATH.find_file(entry) + if found is not None: + resolved.append(str(found)) + else: + resolved.append(entry) + return resolved @classmethod def from_dict(cls, preset_dict, name=None, _exclude=None, _log=False): @@ -655,10 +663,38 @@ def from_dict(cls, preset_dict, name=None, _exclude=None, _log=False): Examples: >>> preset = Preset.from_dict({"target": ["evilcorp.com"], "modules": ["portscan"]}) """ + from bbot.core.helpers.misc import chain_lists + + # Handle seeds and targets from dict + # for user-friendliness, we allow both "target" and "targets" to be used. we merge them into a single list. + target_vals = (preset_dict.get("target") or []) + (preset_dict.get("targets") or []) + # resolve relative file paths via PresetPath (which knows the preset's directory) + targets = chain_lists( + cls._resolve_file_entries(target_vals), + try_files=True, + msg="Reading targets from preset file: {filename}", + _strip_comments=True, + ) + seeds = preset_dict.get("seeds") + if seeds is not None: + seeds = chain_lists( + cls._resolve_file_entries(seeds), + try_files=True, + msg="Reading seeds from preset file: {filename}", + _strip_comments=True, + ) + blacklist = preset_dict.get("blacklist") + if blacklist is not None: + blacklist = chain_lists( + cls._resolve_file_entries(blacklist), + try_files=True, + msg="Reading blacklist from preset file: {filename}", + _strip_comments=True, + ) new_preset = cls( - *preset_dict.get("target", []), - whitelist=preset_dict.get("whitelist"), - blacklist=preset_dict.get("blacklist"), + *targets, + seeds=seeds, + blacklist=blacklist, modules=preset_dict.get("modules"), output_modules=preset_dict.get("output_modules"), exclude_modules=preset_dict.get("exclude_modules"), @@ -728,7 +764,10 @@ def from_yaml_file(cls, filename, _exclude=None, _log=False): except FileNotFoundError: raise PresetNotFoundError(f'Could not find preset at "{filename}" - file does not exist') preset = cls.from_dict( - omegaconf.OmegaConf.create(yaml_str), name=filename.stem, _exclude=_exclude, _log=_log + omegaconf.OmegaConf.create(yaml_str), + name=filename.stem, + _exclude=_exclude, + _log=_log, ) preset._yaml_str = yaml_str preset.filename = filename @@ -757,7 +796,7 @@ def to_dict(self, include_target=False, full_config=False, redact_secrets=False) Convert this preset into a Python dictionary. Args: - include_target (bool, optional): If True, include target, whitelist, and blacklist in the dictionary + include_target (bool, optional): If True, include seeds, target, and blacklist in the dictionary full_config (bool, optional): If True, include the entire config, not just what's changed from the defaults. Returns: @@ -786,15 +825,15 @@ def to_dict(self, include_target=False, full_config=False, redact_secrets=False) # scope if include_target: - target = sorted(self.target.seeds.inputs) - whitelist = [] - if self.target.whitelist is not None: - whitelist = sorted(self.target.whitelist.inputs) + target = sorted(self.target.target.inputs) + seeds = [] + if self.target.seeds is not None: + seeds = sorted(self.target.seeds.inputs) blacklist = sorted(self.target.blacklist.inputs) if target: preset_dict["target"] = target - if whitelist and whitelist != target: - preset_dict["whitelist"] = whitelist + if seeds and seeds != target: + preset_dict["seeds"] = seeds if blacklist: preset_dict["blacklist"] = blacklist @@ -824,7 +863,7 @@ def to_dict(self, include_target=False, full_config=False, redact_secrets=False) if self.scan_name: preset_dict["scan_name"] = self.scan_name if self.scan_name and self.output_dir is not None: - preset_dict["output_dir"] = self.output_dir + preset_dict["output_dir"] = str(self.output_dir) # conditions if self.conditions: @@ -837,7 +876,7 @@ def to_yaml(self, include_target=False, full_config=False, sort_keys=False): Return the preset in the form of a YAML string. Args: - include_target (bool, optional): If True, include target, whitelist, and blacklist in the dictionary + include_target (bool, optional): If True, include seeds, target, and blacklist in the dictionary full_config (bool, optional): If True, include the entire config, not just what's changed from the defaults. sort_keys (bool, optional): If True, sort YAML keys alphabetically @@ -986,11 +1025,13 @@ def presets_table(self, include_modules=True): if include_modules: header.append("Modules") for loaded_preset, category, preset_path, original_file in self.all_presets.values(): - loaded_preset = loaded_preset.bake() - num_modules = f"{len(loaded_preset.scan_modules):,}" + # Use explicit_scan_modules which contains the raw modules from YAML + # This avoids needing to call bake() + explicit_modules = loaded_preset.explicit_scan_modules + num_modules = f"{len(explicit_modules):,}" row = [loaded_preset.name, category, loaded_preset.description, num_modules] if include_modules: - row.append(", ".join(sorted(loaded_preset.scan_modules))) + row.append(", ".join(sorted(explicit_modules))) table.append(row) return make_table(table, header) diff --git a/bbot/scanner/scanner.py b/bbot/scanner/scanner.py index 67c09ea38d..76554a75d5 100644 --- a/bbot/scanner/scanner.py +++ b/bbot/scanner/scanner.py @@ -7,6 +7,7 @@ from pathlib import Path from sys import exc_info from datetime import datetime +from zoneinfo import ZoneInfo from collections import OrderedDict from bbot import __version__ @@ -17,7 +18,20 @@ from bbot.core.config.logger import GzipRotatingFileHandler from bbot.core.multiprocess import SHARED_INTERPRETER_STATE from bbot.core.helpers.async_helpers import async_to_sync_gen +from bbot.logger import log_to_stderr from bbot.errors import BBOTError, ScanError, ValidationError +from bbot.constants import ( + get_scan_status_code, + get_scan_status_name, + SCAN_STATUS_NOT_STARTED, + SCAN_STATUS_STARTING, + SCAN_STATUS_RUNNING, + SCAN_STATUS_FINISHING, + SCAN_STATUS_ABORTING, + SCAN_STATUS_ABORTED, + SCAN_STATUS_FAILED, + SCAN_STATUS_FINISHED, +) log = logging.getLogger("bbot.scanner") @@ -54,7 +68,6 @@ class Scanner: - "STARTING" (1): Status when the scan is initializing. - "RUNNING" (2): Status when the scan is in progress. - "FINISHING" (3): Status when the scan is in the process of finalizing. - - "CLEANING_UP" (4): Status when the scan is cleaning up resources. - "ABORTING" (5): Status when the scan is in the process of being aborted. - "ABORTED" (6): Status when the scan has been aborted. - "FAILED" (7): Status when the scan has encountered a failure. @@ -64,7 +77,7 @@ class Scanner: target (Target): Target of scan (alias to `self.preset.target`). preset (Preset): The main scan Preset in its baked form. config (omegaconf.dictconfig.DictConfig): BBOT config (alias to `self.preset.config`). - whitelist (Target): Scan whitelist (by default this is the same as `target`) (alias to `self.preset.whitelist`). + seeds (Target): Scan seeds (by default this is the same as `target`) (alias to `self.preset.seeds`). blacklist (Target): Scan blacklist (this takes ultimate precedence) (alias to `self.preset.blacklist`). helpers (ConfigAwareHelper): Helper containing various reusable functions, regexes, etc. (alias to `self.preset.helpers`). output_dir (pathlib.Path): Output directory for scan (alias to `self.preset.output_dir`). @@ -84,18 +97,6 @@ class Scanner: - Setting a status will trigger the `on_status` event in the dispatcher. """ - _status_codes = { - "NOT_STARTED": 0, - "STARTING": 1, - "RUNNING": 2, - "FINISHING": 3, - "CLEANING_UP": 4, - "ABORTING": 5, - "ABORTED": 6, - "FAILED": 7, - "FINISHED": 8, - } - def __init__( self, *targets, @@ -127,6 +128,8 @@ def __init__( self._success = False self._scan_finish_status_message = None + self._marked_finished = False + self._modules_loaded = False if scan_id is not None: self.id = str(scan_id) @@ -150,6 +153,15 @@ def __init__( self.preset = base_preset.bake(self) + self._prepped = False + self._finished_init = False + self._new_activity = False + self._cleanedup = False + self._omitted_event_types = None + self.modules = OrderedDict({}) + self.dummy_modules = {} + self._status_code = SCAN_STATUS_NOT_STARTED + # scan name if self.preset.scan_name is None: tries = 0 @@ -169,6 +181,20 @@ def __init__( scan_name = str(self.preset.scan_name) self.name = scan_name.replace("/", "_") + # :) + if self.name == "golden_gus": + from base64 import b64decode as _d + + _a = _d( + "ICAgICAgICAgICAgICBfX18KICAqd29vZiogIF9fL18gIGAuICAuLSIiIi0uCiAgICAgICAgICBcXyxgIHwgXC0nICAvICAgKWAtJykKICAgICAgICAgICAiIikgImAiICAgIFwgICgoImAiCiAgICAgICAgICBfX19ZICAsICAgIC4nNyAvfAogICAgICAgICAoXyxfX18vLi4uLWAgKF8vXy8=" + ).decode() + _m = _d("R3VzIGhhcyBibGVzc2VkIHlvdXIgc2Nhbi4=").decode() + log_to_stderr( + f"\033[1;38;5;220m{_a}\033[0m\n \033[1;38;5;118m{_m}\033[0m", + level="HUGESUCCESS", + logname=False, + ) + # make sure the preset has a description if not self.preset.description: self.preset.description = self.name @@ -179,17 +205,12 @@ def __init__( else: self.home = self.preset.bbot_home / "scans" / self.name + self._status_code = SCAN_STATUS_NOT_STARTED + # scan temp dir self.temp_dir = self.home / "temp" - self.helpers.mkdir(self.temp_dir) - - self._status = "NOT_STARTED" - self._status_code = 0 - - self.modules = OrderedDict({}) - self._modules_loaded = False - self.dummy_modules = {} + # dispatcher if dispatcher is None: from .dispatcher import Dispatcher @@ -204,26 +225,26 @@ def __init__( self.scope_report_distance = int(self.scope_config.get("report_distance", 1)) # web config - self.web_config = self.config.get("web", {}) - self.web_spider_distance = self.web_config.get("spider_distance", 0) - self.web_spider_depth = self.web_config.get("spider_depth", 1) - self.web_spider_links_per_page = self.web_config.get("spider_links_per_page", 20) - max_redirects = self.web_config.get("http_max_redirects", 5) + web_config = self.config.get("web", {}) + self.web_spider_distance = web_config.get("spider_distance", 0) + self.web_spider_depth = web_config.get("spider_depth", 1) + self.web_spider_links_per_page = web_config.get("spider_links_per_page", 20) + max_redirects = web_config.get("http_max_redirects", 5) self.web_max_redirects = max(max_redirects, self.web_spider_distance) - self.http_proxy = self.web_config.get("http_proxy", "") - self.http_timeout = self.web_config.get("http_timeout", 10) - self.httpx_timeout = self.web_config.get("httpx_timeout", 5) - self.http_retries = self.web_config.get("http_retries", 1) - self.httpx_retries = self.web_config.get("httpx_retries", 1) - self.useragent = self.web_config.get("user_agent", "BBOT") + self.http_proxy = web_config.get("http_proxy", "") + self.http_timeout = web_config.get("http_timeout", 10) + self.httpx_timeout = web_config.get("httpx_timeout", 5) + self.http_retries = web_config.get("http_retries", 1) + self.httpx_retries = web_config.get("httpx_retries", 1) + self.useragent = f"{web_config.get('user_agent', 'BBOT')} {web_config.get('user_agent_suffix') or ''}".strip() # custom HTTP headers warning - self.custom_http_headers = self.web_config.get("http_headers", {}) + self.custom_http_headers = web_config.get("http_headers", {}) if self.custom_http_headers: self.warning( "You have enabled custom HTTP headers. These will be attached to all in-scope requests and all requests made by httpx." ) # custom HTTP cookies warning - self.custom_http_cookies = self.web_config.get("http_cookies", {}) + self.custom_http_cookies = web_config.get("http_cookies", {}) if self.custom_http_cookies: self.warning( "You have enabled custom HTTP cookies. These will be attached to all in-scope requests and all requests made by httpx." @@ -247,14 +268,9 @@ def __init__( self.stats = ScanStats(self) - self._prepped = False - self._finished_init = False - self._new_activity = False - self._cleanedup = False - self._omitted_event_types = None - self.init_events_task = None self.ticker_task = None + self._stop_task = None self.dispatcher_tasks = [] self._stopping = False @@ -268,27 +284,48 @@ def __init__( self.__log_handlers = None self._log_handler_backup = [] + # update the master PID + SHARED_INTERPRETER_STATE.update_scan_pid() + async def _prep(self): """ - Creates the scan's output folder, loads its modules, and calls their .setup() methods. + Expands async seed types (e.g. ASN → IP ranges), evaluates preset conditions, + creates the scan's output folder, loads its modules, and calls their .setup() methods. """ + # expand async seed types (e.g. ASN → IP ranges) + ssl_verify = self.preset.web_config.get("ssl_verify", False) + await self.preset.target.generate_children(ssl_verify=ssl_verify) - # update the master PID - SHARED_INTERPRETER_STATE.update_scan_pid() + # evaluate preset conditions (may abort the scan) + if self.preset.conditions: + from .preset.conditions import ConditionEvaluator + + evaluator = ConditionEvaluator(self.preset) + evaluator.evaluate() self.helpers.mkdir(self.home) + self.helpers.mkdir(self.temp_dir) + + if not self._modules_loaded: + self.modules = OrderedDict({}) + self.dummy_modules = {} + if not self._prepped: + # clear modules for fresh start + self.modules.clear() + self.dummy_modules.clear() + # save scan preset with open(self.home / "preset.yml", "w") as f: f.write(self.preset.to_yaml()) # log scan overview - start_msg = f"Scan seeded with {len(self.seeds):,} targets" + start_msg = f"Scan seeded with {len(self.seeds.event_seeds):,} seed(s)" details = [] - if self.whitelist != self.target: - details.append(f"{len(self.whitelist):,} in whitelist") + if self.target.target: + details.append(f"{len(self.target.target.event_seeds):,} in target") if self.blacklist: - details.append(f"{len(self.blacklist):,} in blacklist") + details.append(f"{len(self.blacklist.event_seeds):,} in blacklist") if details: start_msg += f" ({', '.join(details)})" self.hugeinfo(start_msg) @@ -323,9 +360,10 @@ async def _prep(self): self._fail_setup(msg) total_modules = total_failed + len(self.modules) - success_msg = f"Setup succeeded for {len(self.modules):,}/{total_modules:,} modules." + success_msg = f"Setup succeeded for {len(self.modules) - 2:,}/{total_modules - 2:,} modules." self.success(success_msg) + self._modules_loaded = True self._prepped = True def start(self): @@ -341,11 +379,12 @@ async def async_start_without_generator(self): pass async def async_start(self): - """ """ - self.start_time = datetime.now() - self.root_event.data["started_at"] = self.start_time.isoformat() + self.start_time = datetime.now(ZoneInfo("UTC")) try: - await self._prep() + if not self._prepped: + await self._prep() + await self._set_status(SCAN_STATUS_STARTING) + self.root_event.data["started_at"] = self.start_time.timestamp() self._start_log_handlers() self.trace(f"Ran BBOT {__version__} at {self.start_time}, command: {' '.join(sys.argv)}") @@ -360,18 +399,16 @@ async def async_start(self): self._status_ticker(self.status_frequency), name=f"{self.name}._status_ticker()" ) - self.status = "STARTING" - if not self.modules: self.error("No modules loaded") - self.status = "FAILED" + await self._set_status(SCAN_STATUS_FAILED) return else: self.hugesuccess(f"Starting scan {self.name}") await self.dispatcher.on_start(self) - self.status = "RUNNING" + await self._set_status(SCAN_STATUS_RUNNING) self._start_modules() self.verbose(f"{len(self.modules):,} modules started") @@ -401,8 +438,6 @@ async def async_start(self): new_activity = await self.finish() if not new_activity: self._success = True - scan_finish_event = await self._mark_finished() - yield scan_finish_event break await asyncio.sleep(0.1) @@ -411,7 +446,7 @@ async def async_start(self): except BaseException as e: if self.helpers.in_exception_chain(e, (KeyboardInterrupt, asyncio.CancelledError)): - self.stop() + await self.async_stop() self._success = True else: try: @@ -426,16 +461,20 @@ async def async_start(self): self.critical(f"Unexpected error during scan:\n{traceback.format_exc()}") finally: + scan_finish_event = await self._mark_finished() + yield scan_finish_event tasks = self._cancel_tasks() self.debug(f"Awaiting {len(tasks):,} tasks") - for task in tasks: - # self.debug(f"Awaiting {task}") + if tasks: with contextlib.suppress(BaseException): - await asyncio.wait_for(task, timeout=0.1) + await asyncio.wait_for( + asyncio.gather(*tasks, return_exceptions=True), + timeout=2, + ) self.debug(f"Awaited {len(tasks):,} tasks") await self._report() await self._cleanup() - + # report on final scan status await self.dispatcher.on_finish(self) self._stop_log_handlers() @@ -449,34 +488,44 @@ async def async_start(self): log_fn(self._scan_finish_status_message) async def _mark_finished(self): - if self.status == "ABORTING": - status = "ABORTED" + if self._marked_finished: + return + + self._marked_finished = True + + if self._status_code == SCAN_STATUS_ABORTING: + status_code = SCAN_STATUS_ABORTED elif not self._success: - status = "FAILED" + status_code = SCAN_STATUS_FAILED else: - status = "FINISHED" + status_code = SCAN_STATUS_FINISHED - self.end_time = datetime.now() + status = get_scan_status_name(status_code) + + self.end_time = datetime.now(ZoneInfo("UTC")) self.duration = self.end_time - self.start_time self.duration_seconds = self.duration.total_seconds() self.duration_human = self.helpers.human_timedelta(self.duration) - self._scan_finish_status_message = f"Scan {self.name} completed in {self.duration_human} with status {status}" + self._scan_finish_status_message = ( + f"Scan {self.name} completed in {self.duration_human} with status {self.status}" + ) scan_finish_event = self.finish_event(self._scan_finish_status_message, status) - # queue final scan event with output modules - output_modules = [m for m in self.modules.values() if m._type == "output" and m.name != "python"] - for m in output_modules: - await m.queue_event(scan_finish_event) - # wait until output modules are flushed - while 1: - modules_finished = all(m.finished for m in output_modules) - if modules_finished: - break - await asyncio.sleep(0.05) - - self.status = status + if not self._stopping: + # queue final scan event with output modules + output_modules = [m for m in self.modules.values() if m._type == "output" and m.name != "python"] + for m in output_modules: + await m.queue_event(scan_finish_event) + # wait until output modules are flushed + while 1: + modules_finished = all([m.finished for m in output_modules]) + if modules_finished: + break + await asyncio.sleep(0.05) + + await self._set_status(status) return scan_finish_event def _start_modules(self): @@ -519,7 +568,8 @@ async def setup_modules(self, remove_failed=True, deps_only=False): self.modules[module.name].set_error_state() hard_failed.append(module.name) else: - self.info(f"Setup soft-failed for {module.name}: {msg}") + log_fn = self.warning if module._type == "output" else self.info + log_fn(f"Setup soft-failed for {module.name}: {msg}") soft_failed.append(module.name) if (not status) and (module._intercept or remove_failed): # if a intercept module fails setup, we always remove it @@ -557,6 +607,7 @@ async def load_modules(self): if not self._modules_loaded: if not self.preset.modules: self.warning("No modules to load") + self._modules_loaded = True return if not self.preset.scan_modules: @@ -647,7 +698,7 @@ def num_queued_events(self): total += len(q._queue) return total - def modules_status(self, _log=False): + def modules_status(self, _log=False, detailed=False): finished = True status = {"modules": {}} @@ -694,11 +745,9 @@ def modules_status(self, _log=False): self.info(f"{self.name}: Modules running (incoming:processing:outgoing) {modules_status_str}") else: self.info(f"{self.name}: No modules running") - event_type_summary = sorted(self.stats.events_emitted_by_type.items(), key=lambda x: x[-1], reverse=True) + event_type_summary = self.stats.event_type_summary() if event_type_summary: - self.info( - f"{self.name}: Events produced so far: {', '.join([f'{k}: {v}' for k, v in event_type_summary])}" - ) + self.info(f"{self.name}: Events produced so far: {', '.join(event_type_summary)}") else: self.info(f"{self.name}: No events produced yet") @@ -717,7 +766,7 @@ def modules_status(self, _log=False): f"{self.name}: No events in queue ({self.stats.speedometer.speed:,} processed in the past {self.status_frequency} seconds)" ) - if self.log_level <= logging.DEBUG: + if detailed or self.log_level <= logging.DEBUG: # status debugging scan_active_status = [] scan_active_status.append(f"scan._finished_init: {self._finished_init}") @@ -750,7 +799,7 @@ def modules_status(self, _log=False): return status - def stop(self): + async def async_stop(self): """Stops the in-progress scan and performs necessary cleanup. This method sets the scan's status to "ABORTING," cancels any pending tasks, and drains event queues. It also kills child processes spawned during the scan. @@ -760,7 +809,7 @@ def stop(self): """ if not self._stopping: self._stopping = True - self.status = "ABORTING" + await self._set_status(SCAN_STATUS_ABORTING) self.hugewarning("Aborting scan") self.trace() self._cancel_tasks() @@ -769,6 +818,10 @@ def stop(self): self._drain_queues() self.helpers.kill_children() self.debug("Finished aborting scan") + await self._set_status(SCAN_STATUS_ABORTED) + + def stop(self): + self._stop_task = asyncio.create_task(self.async_stop()) async def finish(self): """Finalizes the scan by invoking the `finished()` method on all active modules if new activity is detected. @@ -785,7 +838,7 @@ async def finish(self): # if new events were generated since last time we were here if self._new_activity: self._new_activity = False - self.status = "FINISHING" + await self._set_status(SCAN_STATUS_FINISHING) # Trigger .finished() on every module and start over log.info("Finishing scan") for module in self.modules.values(): @@ -839,8 +892,10 @@ def _cancel_tasks(self): # ticker if self.ticker_task: tasks.append(self.ticker_task) - # dispatcher - tasks += self.dispatcher_tasks + # stop task + if self._stop_task: + tasks.append(self._stop_task) + self.helpers.cancel_tasks_sync(tasks) # process pool self.helpers.process_pool.shutdown(cancel_futures=True) @@ -869,7 +924,8 @@ async def _cleanup(self): This method is called once at the end of the scan to perform resource cleanup tasks. It is executed regardless of whether the scan was aborted or completed - successfully. The scan status is set to "CLEANING_UP" during the execution. + successfully. + After calling the `cleanup()` method for each module, it performs additional cleanup tasks such as removing the scan's home directory if empty and cleaning old scans. @@ -880,26 +936,31 @@ async def _cleanup(self): # clean up self if not self._cleanedup: self._cleanedup = True - self.status = "CLEANING_UP" + # clean up modules + for mod in self.modules.values(): + await mod._cleanup() # clean up dns engine if self.helpers._dns is not None: await self.helpers.dns.shutdown() # clean up web engine if self.helpers._web is not None: await self.helpers.web.shutdown() - # clean up modules - for mod in self.modules.values(): - await mod._cleanup() - with contextlib.suppress(Exception): - self.home.rmdir() - self.helpers.rm_rf(self.temp_dir, ignore_errors=True) + # In some test paths, `_prep()` is never called, so `home` and + # `temp_dir` may not exist. Treat those as best-effort cleanups. + home = getattr(self, "home", None) + if home is not None: + with contextlib.suppress(Exception): + home.rmdir() + temp_dir = getattr(self, "temp_dir", None) + if temp_dir is not None: + self.helpers.rm_rf(temp_dir, ignore_errors=True) self.helpers.clean_old_scans() def in_scope(self, *args, **kwargs): return self.preset.in_scope(*args, **kwargs) - def whitelisted(self, *args, **kwargs): - return self.preset.whitelisted(*args, **kwargs) + def in_target(self, *args, **kwargs): + return self.preset.in_target(*args, **kwargs) def blacklisted(self, *args, **kwargs): return self.preset.blacklisted(*args, **kwargs) @@ -912,6 +973,10 @@ def core(self): def config(self): return self.preset.core.config + @property + def web_config(self): + return self.config.get("web", {}) + @property def target(self): return self.preset.target @@ -920,10 +985,6 @@ def target(self): def seeds(self): return self.preset.seeds - @property - def whitelist(self): - return self.preset.whitelist - @property def blacklist(self): return self.preset.blacklist @@ -946,19 +1007,19 @@ def stopping(self): @property def stopped(self): - return self._status_code > 5 + return self._status_code >= SCAN_STATUS_ABORTED @property def running(self): - return 0 < self._status_code < 4 + return SCAN_STATUS_STARTING <= self._status_code <= SCAN_STATUS_FINISHING @property def aborting(self): - return 5 <= self._status_code <= 6 + return SCAN_STATUS_ABORTING <= self._status_code <= SCAN_STATUS_ABORTED @property def status(self): - return self._status + return get_scan_status_name(self._status_code) @property def omitted_event_types(self): @@ -966,29 +1027,22 @@ def omitted_event_types(self): self._omitted_event_types = self.config.get("omit_event_types", []) return self._omitted_event_types - @status.setter - def status(self, status): - """ - Block setting after status has been aborted - """ - status = str(status).strip().upper() - if status in self._status_codes: - if self.status == "ABORTING" and not status == "ABORTED": - self.debug(f'Attempt to set invalid status "{status}" on aborted scan') - else: - if status != self._status: - self._status = status - self._status_code = self._status_codes[status] - self.dispatcher_tasks.append( - asyncio.create_task( - self.dispatcher.catch(self.dispatcher.on_status, self._status, self.id), - name=f"{self.name}.dispatcher.on_status({status})", - ) - ) - else: - self.debug(f'Scan status is already "{status}"') - else: - self.debug(f'Attempt to set invalid status "{status}" on scan') + async def _set_status(self, status): + try: + status_code = get_scan_status_code(status) + status = get_scan_status_name(status_code) + except ValueError: + self.warning(f'Attempt to set invalid status "{status}" on scan') + + self.debug(f"Setting scan status from {self.status} to {status}") + # if the status isn't progressing forward, skip setting it + if status_code <= self._status_code: + self.debug(f'Attempt to set invalid status "{status}" on scan with status "{self.status}"') + return + + self._status_code = status_code + with self.dispatcher.catch(): + await self.dispatcher.on_status(self.status, self.id) def make_event(self, *args, **kwargs): kwargs["scan"] = self @@ -1015,22 +1069,26 @@ def root_event(self): "tags": [ "distance-0" ], - "module": "TARGET", - "module_sequence": "TARGET" + "module": "SEED", + "module_sequence": "SEED" } ``` """ if self._root_event is None: self._root_event = self.make_root_event(f"Scan {self.name} started at {self.start_time}") self._root_event.data["status"] = self.status + self._root_event.data["status_code"] = self._status_code return self._root_event - def finish_event(self, context=None, status=None): + def finish_event(self, context=None, status_code=None): if self._finish_event is None: - if context is None or status is None: - raise ValueError("Must specify context and status") + if context is None or status_code is None: + raise ValueError("Must specify context and status_code") self._finish_event = self.make_root_event(context) + status_code = get_scan_status_code(status_code) + status = get_scan_status_name(status_code) self._finish_event.data["status"] = status + self._finish_event.data["status_code"] = status_code return self._finish_event def make_root_event(self, context): @@ -1039,7 +1097,7 @@ def make_root_event(self, context): root_event.scope_distance = 0 root_event.parent = root_event root_event._dummy = False - root_event.module = self._make_dummy_module(name="TARGET", _type="TARGET") + root_event.module = self._make_dummy_module(name="SEED", _type="SEED") return root_event @property @@ -1048,13 +1106,13 @@ def dns_strings(self): A list of DNS hostname strings generated from the scan target """ if self._dns_strings is None: - dns_whitelist = {t.host for t in self.whitelist if t.host and isinstance(t.host, str)} - dns_whitelist = sorted(dns_whitelist, key=len) - dns_whitelist_set = set() + dns_target = {t.host for t in self.target.target if t.host and isinstance(t.host, str)} + dns_target = sorted(dns_target, key=len) + dns_target_set = set() dns_strings = [] - for t in dns_whitelist: - if not any(x in dns_whitelist_set for x in self.helpers.domain_parents(t, include_self=True)): - dns_whitelist_set.add(t) + for t in dns_target: + if not any(x in dns_target_set for x in self.helpers.domain_parents(t, include_self=True)): + dns_target_set.add(t) dns_strings.append(t) self._dns_strings = dns_strings return self._dns_strings @@ -1153,7 +1211,7 @@ async def extract_in_scope_hostnames(self, s): @property def json(self): """ - A dictionary representation of the scan including its name, ID, targets, whitelist, blacklist, and modules + A dictionary representation of the scan including its name, ID, targets, target, blacklist, and modules """ j = {} for i in ("id", "name"): @@ -1163,9 +1221,9 @@ def json(self): j["target"] = self.preset.target.json j["preset"] = self.preset.to_dict(redact_secrets=True) if self.start_time is not None: - j["started_at"] = self.start_time.isoformat() + j["started_at"] = self.start_time.timestamp() if self.end_time is not None: - j["finished_at"] = self.end_time.isoformat() + j["finished_at"] = self.end_time.timestamp() if self.duration is not None: j["duration_seconds"] = self.duration_seconds if self.duration_human is not None: diff --git a/bbot/scanner/stats.py b/bbot/scanner/stats.py index 38d95032f7..7740bc0877 100644 --- a/bbot/scanner/stats.py +++ b/bbot/scanner/stats.py @@ -4,6 +4,10 @@ log = logging.getLogger("bbot.scanner.stats") +_VERIFIED_TO_UNVERIFIED = { + "URL": "URL_UNVERIFIED", +} + def _increment(d, k): try: @@ -12,6 +16,19 @@ def _increment(d, k): d[k] = 1 +class EventTypeStats: + """Tracks count for an event type and formats it for the status line.""" + + def __init__(self): + self.count = 0 + + def increment(self, event): + self.count += 1 + + def format(self, event_type): + return f"{event_type}: {self.count}" + + class SpeedCounter: """ A simple class for keeping a rolling tally of the number of events inside a specific time window @@ -41,11 +58,44 @@ def __init__(self, scan): self.scan = scan self.module_stats = {} self.events_emitted_by_type = {} + self._type_stats = {} self.speedometer = SpeedCounter(scan.status_frequency) + def _get_type_stats(self, event): + event_type = event.type + try: + return self._type_stats[event_type] + except KeyError: + stats_class = getattr(event, "_stats_class", None) or EventTypeStats + self._type_stats[event_type] = stats_class() + return self._type_stats[event_type] + + def event_type_summary(self): + """Return a formatted list of event type counts, sorted by count descending.""" + entries = sorted(self._type_stats.items(), key=lambda x: x[1].count, reverse=True) + return [stats.format(event_type) for event_type, stats in entries if stats.count > 0] + + def _get_attribution_module(self, event): + """Return the module that should get credit for producing this event. + + For verified event types (e.g. URL verified from URL_UNVERIFIED), credit + goes to the module that originally discovered the unverified form, + unless the discovering module doesn't declare the unverified type in its produced_events. + """ + unverified_type = _VERIFIED_TO_UNVERIFIED.get(event.type) + if unverified_type is not None: + parent = getattr(event, "parent", None) + if parent is not None and getattr(parent, "type", None) == unverified_type: + parent_module = getattr(parent, "module", None) + if parent_module is not None and unverified_type in getattr(parent_module, "produced_events", []): + return parent_module + return event.module + def event_produced(self, event): _increment(self.events_emitted_by_type, event.type) - module_stat = self.get(event.module) + self._get_type_stats(event).increment(event) + module = self._get_attribution_module(event) + module_stat = self.get(module) if module_stat is not None: module_stat.increment_produced(event) @@ -72,7 +122,7 @@ def table(self): header = ["Module", "Produced", "Consumed"] table = [] for mname, mstat in self.module_stats.items(): - if mname == "TARGET" or mstat.module._stats_exclude: + if mname == "SEED" or mstat.module._stats_exclude: continue table_row = [] table_row.append(mname) diff --git a/bbot/scanner/target.py b/bbot/scanner/target.py index c27fde8b29..d029b6f567 100644 --- a/bbot/scanner/target.py +++ b/bbot/scanner/target.py @@ -1,108 +1,200 @@ import logging import regex as re -from hashlib import sha1 from radixtarget import RadixTarget -from radixtarget.helpers import host_size_key +from radixtarget import host_size_key + + +def _fnv1a_64(data_strings): + """FNV-1a 64-bit hash over sorted strings. Deterministic, pure Python.""" + h = 0xCBF29CE484222325 + for s in data_strings: + for b in s.encode(): + h ^= b + h = (h * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF + return h + from bbot.errors import * from bbot.core.event import is_event from bbot.core.event.helpers import EventSeed, BaseEventSeed -from bbot.core.helpers.misc import is_dns_name, is_ip, is_ip_type +from bbot.core.helpers.misc import is_dns_name, is_ip, is_ip_type, strip_comments log = logging.getLogger("bbot.core.target") -class BaseTarget(RadixTarget): +def _host_str(value): + """Extract a plain host string from any BBOT input type. + + Handles events, EventSeeds, ipaddress objects, and complex strings (URLs, emails, host:port). + Returns None if no host can be extracted. + """ + if value is None: + return None + if is_event(value) or isinstance(value, BaseEventSeed): + h = value.host + return str(h) if h else None + if is_ip(value, include_network=True) or is_dns_name(value): + return str(value) + if isinstance(value, str): + try: + event_seed = EventSeed(value) + h = event_seed.host + return str(h) if h else None + except ValidationError: + return None + return None + + +class BaseTarget: """ A collection of BBOT events that represent a scan target. - The purpose of this class is to hold a potentially huge target list in a space-efficient way, - while allowing lightning fast scope lookups. + Uses a RadixTarget internally for fast scope lookups, and layers on + BBOT-specific parsing (events, URLs, emails, host:port) and hashing. This class is inherited by all three components of the BBOT target: - - Whitelist + - Target - Blacklist - Seeds """ accept_target_types = ["TARGET"] - def __init__(self, *targets, **kwargs): - # ignore blank targets (sometimes happens as a symptom of .splitlines()) - targets = [stripped for t in targets if (stripped := (t.strip() if isinstance(t, str) else t))] + def __init__(self, *targets, strict_scope=False, acl_mode=False): + # strip comments and ignore blank targets + targets = [stripped for t in targets if (stripped := (strip_comments(t).strip() if isinstance(t, str) else t))] + self.strict_scope = strict_scope + self._rt = RadixTarget(strict_scope=strict_scope, acl_mode=acl_mode) self.event_seeds = set() - super().__init__(*targets, **kwargs) + if targets: + self.add(list(targets)) @property def inputs(self): return set(e.input for e in self.event_seeds) + @property + def hosts(self): + return set(self._rt.hosts) + + @property + def hash(self): + h = self._rt.hash + if self.strict_scope: + h ^= 1 + return h.to_bytes(8, "big", signed=True) + def get(self, event, **kwargs): + """Look up a host in the radix tree. + + Accepts events, URLs, emails, host:port strings, IPs, CIDRs, and hostnames. + Returns the stored data for the matching host, or None. """ - Here we override RadixTarget's get() method, which normally only accepts hosts, to also accept events for convenience. - """ - host = None raise_error = kwargs.get("raise_error", False) - # if it's already an event or event seed, use its host - if is_event(event) or isinstance(event, BaseEventSeed): - host = event.host - # save resources by checking if the event is an IP or DNS name - elif is_ip(event, include_network=True) or is_dns_name(event): - host = event - # if it's a string, autodetect its type and parse out its host - elif isinstance(event, str): - event_seed = self._make_event_seed(event, raise_error=raise_error) - host = event_seed.host - if not host: - return - else: - raise ValueError(f"Invalid target type for {self.__class__.__name__}: {type(event)}") - if not host: - msg = f"Host not found: '{event}'" + host_str = _host_str(event) + if host_str is None: if raise_error: - raise KeyError(msg) - else: - log.warning(msg) - return - results = super().get(host, **kwargs) - return results - - def _make_event_seed(self, target, raise_error=False): - try: - return EventSeed(target) - except ValidationError: - msg = f"Invalid target: '{target}'" - if raise_error: - raise KeyError(msg) - else: - log.warning(msg) + raise KeyError(f"Host not found: '{event}'") + return None + return self._rt.get(host_str) def add(self, targets, data=None): if not isinstance(targets, (list, set, tuple)): targets = [targets] event_seeds = set() for target in targets: - event_seed = EventSeed(target) + # accept pre-parsed EventSeed objects to avoid expensive re-parsing + if isinstance(target, BaseEventSeed): + event_seed = target + else: + event_seed = EventSeed(target) if not event_seed._target_type in self.accept_target_types: log.warning(f"Invalid target type for {self.__class__.__name__}: {event_seed.type}") continue event_seeds.add(event_seed) # sort by host size to ensure consistency - event_seeds = sorted(event_seeds, key=lambda e: (0, 0) if not e.host else host_size_key(e.host)) + event_seeds = sorted(event_seeds, key=lambda e: (0, 0) if not e.host else host_size_key(str(e.host))) for event_seed in event_seeds: self.event_seeds.add(event_seed) + # Some event seeds (e.g. ORG_STUB, USERNAME, BLACKLIST_REGEX) are not host-based and have + # host == None. These are still useful as parsed target entries, but cannot always be + # represented in the underlying RadixTarget tree, which expects a concrete host. + # Subclasses like ScanBlacklist may still need to see these entries (for regex handling, + # etc.), so we always call self._add() and let the subclass decide whether to forward to + # the radix layer. self._add(event_seed.host, data=(event_seed if data is None else data)) + def _add(self, host, data): + """Insert a host into the radix tree. + + The radix tree cannot handle host == None, but some subclasses (e.g. ScanBlacklist) + need to receive non-host-based entries such as BLACKLIST_REGEX. BaseTarget.add() + always calls self._add(); this default implementation safely ignores hostless + entries while still delegating normal hosts to the radix tree. + """ + if host is None: + return + self._rt.insert(str(host), data=data) + + def _make_event_seed(self, target, raise_error=False): + try: + return EventSeed(target) + except ValidationError: + msg = f"Invalid target: '{target}'" + if raise_error: + raise KeyError(msg) + else: + log.warning(msg) + + def __contains__(self, other): + if isinstance(other, BaseTarget): + for h in other.hosts: + if self.get(str(h)) is None: + return False + return True + try: + return self.get(other) is not None + except (ValueError, TypeError): + return False + def __iter__(self): yield from self.event_seeds + def __len__(self): + return len(self._rt) + + def __bool__(self): + return bool(len(self._rt)) or bool(self.event_seeds) + + def __getstate__(self): + return { + "event_seeds": self.event_seeds, + "strict_scope": self.strict_scope, + "acl_mode": self._rt._acl_mode, + } + + def __setstate__(self, state): + self.strict_scope = state["strict_scope"] + self._rt = RadixTarget(strict_scope=state["strict_scope"], acl_mode=state["acl_mode"]) + self.event_seeds = set() + for event_seed in state["event_seeds"]: + self.event_seeds.add(event_seed) + self._add(event_seed.host, data=event_seed) + + def __eq__(self, other): + return self.hash == getattr(other, "hash", None) + + def __hash__(self): + return hash(self.hash) + class ScanSeeds(BaseTarget): """ Initial events used to seed a scan. - These are the targets specified by the user, e.g. via `-t` on the CLI. + These are the seeds specified by the user, e.g. via `-s` on the CLI. + If no seeds were specified, the targets (`-t`) are copied here. """ def get(self, event, single=True, **kwargs): @@ -121,28 +213,35 @@ def _add(self, host, data): as separate events even though they have the same host. """ if host: - try: - event_set = self.get(host, raise_error=True, single=False) - event_set.add(data) - except KeyError: + existing = self.get(str(host), raise_error=False, single=False) + if existing is not None: + existing.add(data) + event_set = existing + else: event_set = {data} super()._add(host, data=event_set) - def _hash_value(self): - # seeds get hashed by event data - return sorted(str(e.data).encode() for e in self.event_seeds) + @property + def hash(self): + """Seeds get hashed by event data, not by hosts.""" + h = _fnv1a_64(sorted(str(e.data) for e in self.event_seeds)) + return h.to_bytes(8, "big") class ACLTarget(BaseTarget): def __init__(self, *args, **kwargs): - # ACL mode dedupes by host (and skips adding already-contained hosts) for efficiency - kwargs["acl_mode"] = True + # acl_mode deduplicates (parent absorbs child), but it's mutually exclusive + # with strict_scope in radixtarget 4.x. When strict_scope is enabled, + # we skip acl_mode — DNS entries need to remain separate for exact matching, + # and IP range dedup is handled naturally by the radix tree. + if not kwargs.get("strict_scope", False): + kwargs["acl_mode"] = True super().__init__(*args, **kwargs) -class ScanWhitelist(ACLTarget): +class ScanTarget(ACLTarget): """ - A collection of BBOT events that represent a scan's whitelist. + A collection of BBOT events that represent a scan's targets. """ pass @@ -159,6 +258,10 @@ def __init__(self, *args, **kwargs): self.blacklist_regexes = set() super().__init__(*args, **kwargs) + def __setstate__(self, state): + self.blacklist_regexes = set() + super().__setstate__(state) + def get(self, host, **kwargs): """ Blacklists only accept IPs or strings. This is cleaner since we need to search for regex patterns. @@ -173,10 +276,7 @@ def get(self, host, **kwargs): to_match = event_seed.data except ValidationError: to_match = str(host) - try: - event_result = super().get(host, raise_error=True) - except KeyError: - event_result = None + event_result = super().get(host) if event_result is not None: return event_result # next, check event's host against regexes @@ -193,14 +293,14 @@ def _add(self, host, data): if host is not None: super()._add(host, data) - def _hash_value(self): - # regexes are included in blacklist hash - regex_patterns = [str(r.pattern).encode() for r in self.blacklist_regexes] - hosts = [str(h).encode() for h in self.sorted_hosts] - return hosts + regex_patterns + @property + def hash(self): + """Blacklist hash includes both hosts and regex patterns.""" + h = (self._rt.hash ^ _fnv1a_64(sorted(r.pattern for r in self.blacklist_regexes))) & 0xFFFFFFFFFFFFFFFF + return h.to_bytes(8, "big") def __len__(self): - return super().__len__() + len(self.blacklist_regexes) + return len(self._rt) + len(self.blacklist_regexes) def __bool__(self): return bool(len(self)) @@ -210,67 +310,72 @@ class BBOTTarget: """ A convenient abstraction of a scan target that contains three subtargets: - seeds - - whitelist + - target - blacklist - Provides high-level functions like in_scope(), which includes both whitelist and blacklist checks. + Provides high-level functions like in_scope(), which includes both target and blacklist checks. """ - def __init__(self, *seeds, whitelist=None, blacklist=None, strict_scope=False): + def __init__(self, seeds=None, target=None, blacklist=None, strict_scope=False): self.strict_scope = strict_scope - self.seeds = ScanSeeds(*seeds, strict_dns_scope=strict_scope) - if whitelist is None: - whitelist = self.seeds.hosts - self.whitelist = ScanWhitelist(*whitelist, strict_dns_scope=strict_scope) - if blacklist is None: - blacklist = [] - self.blacklist = ScanBlacklist(*blacklist) + self._orig_seeds = seeds + + target_list = list(target) if target else [] + self.target = ScanTarget(*target_list, strict_scope=strict_scope) + + # Seeds are only copied from target if target is defined but seeds are NOT defined + # Pass pre-parsed event_seeds to avoid expensive re-parsing of every target string + if seeds is None: + seeds = list(self.target.event_seeds) + self.seeds = ScanSeeds(*list(seeds), strict_scope=strict_scope) + + blacklist_list = list(blacklist) if blacklist else [] + self.blacklist = ScanBlacklist(*blacklist_list) @property def json(self): - return { - "seeds": sorted(self.seeds.inputs), - "whitelist": sorted(self.whitelist.inputs), + j = { + "target": sorted(self.target.inputs), "blacklist": sorted(self.blacklist.inputs), "strict_scope": self.strict_scope, "hash": self.hash.hex(), "seed_hash": self.seeds.hash.hex(), - "whitelist_hash": self.whitelist.hash.hex(), + "target_hash": self.target.hash.hex(), "blacklist_hash": self.blacklist.hash.hex(), "scope_hash": self.scope_hash.hex(), } + if self._orig_seeds is not None: + j["seeds"] = sorted(self.seeds.inputs) + return j @property def hash(self): - sha1_hash = sha1() - for target_hash in [t.hash for t in (self.seeds, self.whitelist, self.blacklist)]: - sha1_hash.update(target_hash) - return sha1_hash.digest() + return b"".join(t.hash for t in (self.seeds, self.target, self.blacklist)) @property def scope_hash(self): - sha1_hash = sha1() - # Consider only the hash values of the whitelist and blacklist - for target_hash in [t.hash for t in (self.whitelist, self.blacklist)]: - sha1_hash.update(target_hash) - return sha1_hash.digest() + return b"".join(t.hash for t in (self.target, self.blacklist)) def in_scope(self, host): """ Check whether a hostname, url, IP, etc. is in scope. Accepts either events or string data. - Checks whitelist and blacklist. - If `host` is an event and its scope distance is zero, it will automatically be considered in-scope. + This method checks both target AND blacklist. + A host is in-scope if it is in the target AND not blacklisted. + + Note: This is different from `in_target()` which only checks the target. + - `in_target()`: checks if host is in the target + - `in_scope()`: checks if host is in the target AND not blacklisted Examples: Check if a URL is in scope: >>> preset.in_scope("http://www.evilcorp.com") True """ - blacklisted = self.blacklisted(host) - whitelisted = self.whitelisted(host) - return whitelisted and not blacklisted + if self.blacklisted(host): + return False + return self.in_target(host) def blacklisted(self, host): """ @@ -286,23 +391,55 @@ def blacklisted(self, host): >>> preset.blacklisted("http://www.evilcorp.com") True """ - return host in self.blacklist + return self.blacklist.get(host) is not None - def whitelisted(self, host): + def in_target(self, host): """ - Check whether a hostname, url, IP, etc. is whitelisted. + Check whether a hostname, url, IP, etc. is in the target. + + This method ONLY checks the target, NOT the blacklist. + Use `in_scope()` to check both target AND blacklist. Note that `host` can be a hostname, IP address, CIDR, email address, or any BBOT `Event` with the `host` attribute. Args: - host (str or IPAddress or Event): The host to check against the whitelist + host (str or IPAddress or Event): The host to check against the target Examples: - Check if a URL's host is whitelisted: - >>> preset.whitelisted("http://www.evilcorp.com") + Check if a URL's host is in target: + >>> preset.in_target("http://www.evilcorp.com") True """ - return host in self.whitelist + return self.target.get(host) is not None def __eq__(self, other): return self.hash == other.hash + + async def generate_children(self, ssl_verify=False): + """ + Generate children for the target, for seed types that expand into other seed types. + E.g. ASN targets are expanded into their constituent IP ranges. + """ + # If the user explicitly set a narrower target scope than their seeds + # (e.g. `-t evilcorp.com -s AS1234`), don't widen the target with expanded seeds + has_explicit_scope = set(self.target.inputs) != set(self.seeds.inputs) + + # Expand seeds first + for event_seed in list(self.seeds.event_seeds): + children = await event_seed._generate_children(ssl_verify=ssl_verify) + for child in children: + self.seeds.add(child) + + # Also expand blacklist event seeds (like ASN targets) + for event_seed in list(self.blacklist.event_seeds): + children = await event_seed._generate_children(ssl_verify=ssl_verify) + for child in children: + self.blacklist.add(child) + + # Widen target scope to include expanded seed hosts (e.g. IP ranges from ASN), + # but only when seeds and target were originally the same + if not has_explicit_scope: + expanded_seed_hosts = set(self.seeds.hosts) + for host in expanded_seed_hosts: + if host not in self.target: + self.target.add(host) diff --git a/bbot/scripts/benchmark_report.py b/bbot/scripts/benchmark_report.py index 9ccf30a198..50ca6f3384 100644 --- a/bbot/scripts/benchmark_report.py +++ b/bbot/scripts/benchmark_report.py @@ -33,7 +33,12 @@ def get_current_branch() -> str: def checkout_branch(branch: str, repo_path: Path = None): - """Checkout a git branch.""" + """Checkout a git branch, cleaning up generated files first.""" + # Remove untracked files before checkout. Without this, files generated + # by one branch's toolchain (e.g. uv.lock from `uv run` on a Poetry + # branch) block checkout to a branch that tracks those same files. + print("Cleaning untracked files before checkout") + run_command(["git", "clean", "-fd"], cwd=repo_path) print(f"Checking out branch: {branch}") run_command(["git", "checkout", branch], cwd=repo_path) @@ -51,7 +56,7 @@ def run_benchmarks(output_file: Path, repo_path: Path = None) -> bool: try: cmd = [ - "poetry", + "uv", "run", "python", "-m", @@ -175,6 +180,7 @@ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branc |--------------|---------|------------|-----------|-----------|""" significant_changes = [] + new_tests = [] performance_summary = [] for current_bench in current_benchmarks: @@ -183,6 +189,7 @@ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branc test_name = name.replace("test_", "").replace("_", " ").title() current_stats = current_bench.get("stats", {}) + current_extra = current_bench.get("extra_info", {}) current_mean = current_stats.get("mean", 0) # For multi-item benchmarks, calculate correct ops/sec if "excavate" in name: @@ -195,6 +202,10 @@ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branc current_ops = 100 / current_mean # 100 items per test elif "make_event" in name and "large" in name: current_ops = 1000 / current_mean # 1000 items per test + elif "event_memory" in name and "medium" in name: + current_ops = 10000 / current_mean # 10K events per test + elif "event_memory" in name and "large" in name: + current_ops = 50000 / current_mean # 50K events per test elif "ip" in name: current_ops = 1000 / current_mean # 1000 IPs per test elif "bloom_filter" in name: @@ -208,6 +219,7 @@ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branc base_bench = base_lookup.get(name) if base_bench: base_stats = base_bench.get("stats", {}) + base_extra = base_bench.get("extra_info", {}) base_mean = base_stats.get("mean", 0) # For multi-item benchmarks, calculate correct ops/sec if "excavate" in name: @@ -220,6 +232,10 @@ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branc base_ops = 100 / base_mean # 100 items per test elif "make_event" in name and "large" in name: base_ops = 1000 / base_mean # 1000 items per test + elif "event_memory" in name and "medium" in name: + base_ops = 10000 / base_mean # 10K events per test + elif "event_memory" in name and "large" in name: + base_ops = 50000 / base_mean # 50K events per test elif "ip" in name: base_ops = 1000 / base_mean # 1000 IPs per test elif "bloom_filter" in name: @@ -230,7 +246,23 @@ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branc else: base_ops = 1 / base_mean # Default: single operation - change_percent, emoji = calculate_change_percentage(base_mean, current_mean) + # Use memory metrics if available, otherwise use time + current_mb = current_extra.get("total_memory_mb") + base_mb = base_extra.get("total_memory_mb") + current_peb = current_extra.get("per_event_bytes") + base_peb = base_extra.get("per_event_bytes") + if current_mb is not None and base_mb is not None and current_peb is None: + change_percent, emoji = calculate_change_percentage(base_mb, current_mb) + base_label = f"{base_mb:.1f} MB" + current_label = f"{current_mb:.1f} MB" + elif current_peb is not None and base_peb is not None: + change_percent, emoji = calculate_change_percentage(base_peb, current_peb) + base_label = f"{base_peb:.0f} B/event" + current_label = f"{current_peb:.0f} B/event" + else: + change_percent, emoji = calculate_change_percentage(base_mean, current_mean) + base_label = format_time(base_mean) + current_label = format_time(current_mean) # Create visual change indicator if abs(change_percent) > 20: @@ -240,11 +272,18 @@ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branc else: change_bar = "⚪" - table += f"\n| **{test_name}** | `{format_time(base_mean)}` | `{format_time(current_mean)}` | **{change_percent:+.1f}%** {change_bar} | {emoji} |" + table += f"\n| **{test_name}** | `{base_label}` | `{current_label}` | **{change_percent:+.1f}%** {change_bar} | {emoji} |" # Track significant changes if abs(change_percent) > 10: - direction = "🐌 slower" if change_percent > 0 else "🚀 faster" + is_memory = ( + current_extra.get("per_event_bytes") is not None + or current_extra.get("total_memory_mb") is not None + ) + if is_memory: + direction = "🐌 more memory" if change_percent > 0 else "🚀 less memory" + else: + direction = "🐌 slower" if change_percent > 0 else "🚀 faster" significant_changes.append(f"- **{test_name}**: {abs(change_percent):.1f}% {direction}") if change_percent > 0: regressions += 1 @@ -263,11 +302,10 @@ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branc "current_ops": current_ops, } ) + else: table += f"\n| **{test_name}** | `-` | `{format_time(current_mean)}` | **New** 🆕 | 🆕 |" - significant_changes.append( - f"- **{test_name}**: New test 🆕 ({format_time(current_mean)}, {format_ops(current_ops)})" - ) + new_tests.append(f"- **{test_name}**: {format_time(current_mean)}, {format_ops(current_ops)}") table += "\n\n\n\n" @@ -293,6 +331,13 @@ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branc table += f"{change}\n" table += "\n" + # Add new tests section + if new_tests: + table += "### 🆕 New Tests\n\n" + for new_test in new_tests: + table += f"{new_test}\n" + table += "\n" + return table @@ -380,9 +425,6 @@ def main(): base_data = {} current_data = {} - base_data = {} - current_data = {} - try: # Run benchmarks on base branch print(f"\n=== Running benchmarks on base branch: {args.base} ===") diff --git a/bbot/scripts/docs.py b/bbot/scripts/docs.py index a0c55d73f2..8da02e83bd 100755 --- a/bbot/scripts/docs.py +++ b/bbot/scripts/docs.py @@ -6,7 +6,7 @@ import yaml from pathlib import Path -from bbot import Preset +from bbot.scanner import Preset from bbot.core.modules import MODULE_LOADER diff --git a/bbot/test/bbot_fixtures.py b/bbot/test/bbot_fixtures.py index 9ad2d932fa..1deca2be08 100644 --- a/bbot/test/bbot_fixtures.py +++ b/bbot/test/bbot_fixtures.py @@ -15,7 +15,6 @@ from bbot.core import CORE from bbot.scanner import Preset from bbot.core.helpers.misc import mkdir, rand_string -from bbot.core.helpers.async_helpers import get_event_loop log = logging.getLogger("bbot.test.fixtures") @@ -55,6 +54,8 @@ def clean_default_config(monkeypatch): ) with monkeypatch.context() as m: m.setattr("bbot.core.core.DEFAULT_CONFIG", clean_config) + # Also clear CORE's custom_config to ensure Preset.copy() gets a clean core + m.setattr(CORE, "_custom_config", OmegaConf.create({})) yield @@ -82,14 +83,14 @@ def bbot_scanner(): @pytest.fixture -def scan(): +async def scan(): from bbot.scanner import Scanner bbot_scan = Scanner("127.0.0.1", modules=["ipneighbor"]) + await bbot_scan._prep() yield bbot_scan - loop = get_event_loop() - loop.run_until_complete(bbot_scan._cleanup()) + await bbot_scan._cleanup() @pytest.fixture @@ -146,48 +147,90 @@ def helpers(scan): @pytest.fixture def events(scan): + dummy_module = scan._make_dummy_module("dummy_module") + class bbot_events: - localhost = scan.make_event("127.0.0.1", parent=scan.root_event) - ipv4 = scan.make_event("8.8.8.8", parent=scan.root_event) - netv4 = scan.make_event("8.8.8.8/30", parent=scan.root_event) - ipv6 = scan.make_event("2001:4860:4860::8888", parent=scan.root_event) - netv6 = scan.make_event("2001:4860:4860::8888/126", parent=scan.root_event) - domain = scan.make_event("publicAPIs.org", parent=scan.root_event) - subdomain = scan.make_event("api.publicAPIs.org", parent=scan.root_event) - email = scan.make_event("bob@evilcorp.co.uk", "EMAIL_ADDRESS", parent=scan.root_event) - open_port = scan.make_event("api.publicAPIs.org:443", parent=scan.root_event) + localhost = scan.make_event("127.0.0.1", parent=scan.root_event, module=dummy_module) + ipv4 = scan.make_event("8.8.8.8", parent=scan.root_event, module=dummy_module) + netv4 = scan.make_event("8.8.8.8/30", parent=scan.root_event, module=dummy_module) + ipv6 = scan.make_event("2001:4860:4860::8888", parent=scan.root_event, module=dummy_module) + netv6 = scan.make_event("2001:4860:4860::8888/126", parent=scan.root_event, module=dummy_module) + domain = scan.make_event("publicAPIs.org", parent=scan.root_event, module=dummy_module) + subdomain = scan.make_event("api.publicAPIs.org", parent=scan.root_event, module=dummy_module) + email = scan.make_event("bob@evilcorp.co.uk", "EMAIL_ADDRESS", parent=scan.root_event, module=dummy_module) + open_port = scan.make_event("api.publicAPIs.org:443", parent=scan.root_event, module=dummy_module) protocol = scan.make_event( - {"host": "api.publicAPIs.org", "port": 443, "protocol": "HTTP"}, "PROTOCOL", parent=scan.root_event + {"host": "api.publicAPIs.org", "port": 443, "protocol": "HTTP"}, + "PROTOCOL", + parent=scan.root_event, + module=dummy_module, + ) + ipv4_open_port = scan.make_event("8.8.8.8:443", parent=scan.root_event, module=dummy_module) + ipv6_open_port = scan.make_event( + "[2001:4860:4860::8888]:443", "OPEN_TCP_PORT", parent=scan.root_event, module=dummy_module + ) + url_unverified = scan.make_event( + "https://api.publicAPIs.org:443/hellofriend", parent=scan.root_event, module=dummy_module + ) + ipv4_url_unverified = scan.make_event( + "https://8.8.8.8:443/hellofriend", parent=scan.root_event, module=dummy_module + ) + ipv6_url_unverified = scan.make_event( + "https://[2001:4860:4860::8888]:443/hellofriend", parent=scan.root_event, module=dummy_module ) - ipv4_open_port = scan.make_event("8.8.8.8:443", parent=scan.root_event) - ipv6_open_port = scan.make_event("[2001:4860:4860::8888]:443", "OPEN_TCP_PORT", parent=scan.root_event) - url_unverified = scan.make_event("https://api.publicAPIs.org:443/hellofriend", parent=scan.root_event) - ipv4_url_unverified = scan.make_event("https://8.8.8.8:443/hellofriend", parent=scan.root_event) - ipv6_url_unverified = scan.make_event("https://[2001:4860:4860::8888]:443/hellofriend", parent=scan.root_event) url = scan.make_event( - "https://api.publicAPIs.org:443/hellofriend", "URL", tags=["status-200"], parent=scan.root_event + "https://api.publicAPIs.org:443/hellofriend", + "URL", + tags=["status-200"], + parent=scan.root_event, + module=dummy_module, ) ipv4_url = scan.make_event( - "https://8.8.8.8:443/hellofriend", "URL", tags=["status-200"], parent=scan.root_event + "https://8.8.8.8:443/hellofriend", "URL", tags=["status-200"], parent=scan.root_event, module=dummy_module ) ipv6_url = scan.make_event( - "https://[2001:4860:4860::8888]:443/hellofriend", "URL", tags=["status-200"], parent=scan.root_event + "https://[2001:4860:4860::8888]:443/hellofriend", + "URL", + tags=["status-200"], + parent=scan.root_event, + module=dummy_module, + ) + url_hint = scan.make_event( + "https://api.publicAPIs.org:443/hello.ash", "URL_HINT", parent=url, module=dummy_module ) url_hint = scan.make_event("https://api.publicAPIs.org:443/hello.ash", "URL_HINT", parent=url) - vulnerability = scan.make_event( - {"host": "evilcorp.com", "severity": "INFO", "description": "asdf"}, - "VULNERABILITY", + finding = scan.make_event( + { + "host": "evilcorp.com", + "severity": "INFO", + "confidence": "HIGH", + "description": "asdf", + "name": "Test Finding", + }, + "FINDING", + parent=scan.root_event, + module=dummy_module, + ) + finding = scan.make_event( + { + "host": "evilcorp.com", + "description": "asdf", + "name": "Finding", + "severity": "INFO", + "confidence": "HIGH", + }, + "FINDING", parent=scan.root_event, + module=dummy_module, ) - finding = scan.make_event({"host": "evilcorp.com", "description": "asdf"}, "FINDING", parent=scan.root_event) - vhost = scan.make_event({"host": "evilcorp.com", "vhost": "www.evilcorp.com"}, "VHOST", parent=scan.root_event) - http_response = scan.make_event(httpx_response, "HTTP_RESPONSE", parent=scan.root_event) + http_response = scan.make_event(httpx_response, "HTTP_RESPONSE", parent=scan.root_event, module=dummy_module) storage_bucket = scan.make_event( {"name": "storage", "url": "https://storage.blob.core.windows.net"}, "STORAGE_BUCKET", parent=scan.root_event, + module=dummy_module, ) - emoji = scan.make_event("💩", "WHERE_IS_YOUR_GOD_NOW", parent=scan.root_event) + emoji = scan.make_event("💩", "WHERE_IS_YOUR_GOD_NOW", parent=scan.root_event, module=dummy_module) bbot_events.all = [ # noqa: F841 bbot_events.localhost, @@ -209,9 +252,7 @@ class bbot_events: bbot_events.ipv4_url, bbot_events.ipv6_url, bbot_events.url_hint, - bbot_events.vulnerability, bbot_events.finding, - bbot_events.vhost, bbot_events.http_response, bbot_events.storage_bucket, bbot_events.emoji, diff --git a/bbot/test/benchmarks/_scan_memory_subdomain_enum.py b/bbot/test/benchmarks/_scan_memory_subdomain_enum.py new file mode 100644 index 0000000000..33b4b997f6 --- /dev/null +++ b/bbot/test/benchmarks/_scan_memory_subdomain_enum.py @@ -0,0 +1,62 @@ +""" +Subprocess script for subdomain enumeration memory benchmark. + +Injects SUBDOMAIN_ENUM_COUNT synthetic DNS_NAME events into a scan +and prints peak tracemalloc memory to stdout. + +Invoked by test_scan_memory.py — not meant to be run directly. +""" + +import gc +import sys +import asyncio +import tracemalloc + +from bbot.scanner import Scanner + +SUBDOMAIN_ENUM_COUNT = int(sys.argv[1]) + +scan = Scanner( + "blacklanternsecurity.com", + modules=[], + output_modules=["python"], + config={ + "dns": {"disable": True}, + "scope": {"search_distance": 0}, + "web": {"spider_distance": 0, "spider_depth": 0}, + "speculate": False, + "excavate": True, + "aggregate": False, + "cloudcheck": False, + }, + force_start=True, +) + + +async def run(): + await scan._prep() + gc.collect() + if tracemalloc.is_tracing(): + tracemalloc.stop() + tracemalloc.start() + events = [] + injected = False + async for event in scan.async_start(): + events.append(event) + if event.type == "SCAN" and not injected: + injected = True + root_event = scan.root_event + for i in range(SUBDOMAIN_ENUM_COUNT): + dns_event = scan.make_event( + f"sub{i}.blacklanternsecurity.com", + "DNS_NAME", + parent=root_event, + context=f"benchmark DNS_NAME {i}", + ) + await scan.ingress_module.queue_event(dns_event, {}) + + +asyncio.run(run()) +_, peak = tracemalloc.get_traced_memory() +tracemalloc.stop() +print(f"PEAK_MB:{round(peak / 1024 / 1024, 2)}") diff --git a/bbot/test/benchmarks/_scan_memory_web_crawl.py b/bbot/test/benchmarks/_scan_memory_web_crawl.py new file mode 100644 index 0000000000..e609220c74 --- /dev/null +++ b/bbot/test/benchmarks/_scan_memory_web_crawl.py @@ -0,0 +1,86 @@ +""" +Subprocess script for web crawl memory benchmark. + +Launches a local HTTP server with NUM_PAGES pages (each BODY_SIZE bytes), +runs a BBOT scan against it, and prints peak tracemalloc memory to stdout. + +Invoked by test_scan_memory.py — not meant to be run directly. +""" + +import gc +import sys +import asyncio +import threading +import tracemalloc +import importlib.util +from http.server import HTTPServer, BaseHTTPRequestHandler + +from bbot.scanner import Scanner + +NUM_PAGES = int(sys.argv[1]) +BODY_SIZE = int(sys.argv[2]) + +HTTP_MODULE = "httpx" if importlib.util.find_spec("bbot.modules.httpx") else "http" + + +class H(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == "/": + links = "".join(f'page{i}' for i in range(NUM_PAGES)) + body = "" + links + "" + elif self.path.startswith("/page"): + i = self.path.replace("/page", "") + links = f'infodetails' + body = "

Page " + i + "

" + links + "A" * BODY_SIZE + "" + elif self.path.startswith("/data"): + body = "data endpoint" + else: + self.send_response(404) + self.end_headers() + return + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.end_headers() + self.wfile.write(body.encode()) + + def log_message(self, *a): + pass + + +server = HTTPServer(("127.0.0.1", 0), H) +port = server.server_address[1] +threading.Thread(target=server.serve_forever, daemon=True).start() + +scan = Scanner( + f"http://127.0.0.1:{port}/", + modules=[HTTP_MODULE], + output_modules=["python"], + config={ + "dns": {"disable": True}, + "scope": {"search_distance": 0}, + "web": {"spider_distance": 10, "spider_depth": 10, "spider_links_per_page": NUM_PAGES}, + "speculate": True, + "excavate": True, + "aggregate": False, + "cloudcheck": False, + }, + force_start=True, +) + + +async def run(): + await scan._prep() + gc.collect() + if tracemalloc.is_tracing(): + tracemalloc.stop() + tracemalloc.start() + events = [] + async for event in scan.async_start(): + events.append(event) + + +asyncio.run(run()) +_, peak = tracemalloc.get_traced_memory() +tracemalloc.stop() +server.shutdown() +print(f"PEAK_MB:{round(peak / 1024 / 1024, 2)}") diff --git a/bbot/test/benchmarks/test_event_memory_benchmarks.py b/bbot/test/benchmarks/test_event_memory_benchmarks.py new file mode 100644 index 0000000000..dca3b5521e --- /dev/null +++ b/bbot/test/benchmarks/test_event_memory_benchmarks.py @@ -0,0 +1,120 @@ +import random +import tracemalloc + +import pytest + +from bbot.core.event.base import make_event + + +class TestEventMemoryBenchmarks: + """ + Benchmark tests for event memory footprint. + + Simulates realistic scan workloads and measures per-event memory consumption. + Uses pytest-benchmark's extra_info to record memory metrics alongside timing data. + """ + + def setup_method(self): + random.seed(42) + self.common_tags = [ + "in-scope", + "distance-0", + "subdomain", + "ipv4", + "resolved", + "a-record", + "aaaa-record", + "private-ip", + "microsoft", + "cdn", + "open-port", + "status-200", + "web", + "dir", + "endpoint", + ] + + def _generate_scan_events(self, count): + """Generate a realistic mix of events simulating a subdomain enum + web crawl scan.""" + rng = random.Random(42) + subdomains = ["www", "api", "mail", "ftp", "admin", "test", "dev", "staging", "blog", "cdn", "assets", "app"] + tlds = ["com", "org", "net", "io"] + events = [] + + for i in range(count): + kind = i % 5 + if kind <= 1: + # DNS_NAME (~40%) + sub = rng.choice(subdomains) + data = f"{sub}{i}.example.{rng.choice(tlds)}" + event_type = "DNS_NAME" + elif kind == 2: + # IP_ADDRESS (~20%) + data = f"{rng.randint(1, 254)}.{rng.randint(1, 254)}.{rng.randint(1, 254)}.{rng.randint(1, 254)}" + event_type = "IP_ADDRESS" + elif kind == 3: + # URL_UNVERIFIED (~20%) + sub = rng.choice(subdomains) + data = f"http://{sub}{i}.example.{rng.choice(tlds)}/path{i}" + event_type = "URL_UNVERIFIED" + else: + # OPEN_TCP_PORT (~20%) + data = f"{rng.choice(subdomains)}{i}.example.{rng.choice(tlds)}:{rng.choice([80, 443, 8080, 8443])}" + event_type = "OPEN_TCP_PORT" + + e = make_event(data, event_type, dummy=True) + # 5-10 tags per event, typical of a real scan + num_tags = 5 + (i % 6) + for tag in self.common_tags[:num_tags]: + e.add_tag(tag) + events.append(e) + + return events + + def _measure_memory(self, count): + """Create count events under tracemalloc and return (events, total_bytes, per_event_bytes).""" + # Warm up to exclude one-time allocations + _warmup = self._generate_scan_events(50) + del _warmup + + tracemalloc.start() + snapshot_before = tracemalloc.take_snapshot() + + events = self._generate_scan_events(count) + + snapshot_after = tracemalloc.take_snapshot() + tracemalloc.stop() + + stats = snapshot_after.compare_to(snapshot_before, "lineno") + total_bytes = sum(s.size_diff for s in stats if s.size_diff > 0) + per_event = total_bytes / count + + return events, total_bytes, per_event + + @pytest.mark.benchmark(group="event_memory_scan_simulation") + def test_event_memory_medium_scan(self, benchmark): + """Simulate a medium scan (~10K events) and measure per-event memory.""" + count = 10_000 + + # Use benchmark for timing the event creation + benchmark.pedantic(lambda: self._generate_scan_events(count), iterations=1, rounds=3) + + # Measure memory separately (tracemalloc + benchmark don't mix well) + events, total_bytes, per_event = self._measure_memory(count) + benchmark.extra_info["total_memory_bytes"] = total_bytes + benchmark.extra_info["per_event_bytes"] = round(per_event, 1) + benchmark.extra_info["total_memory_mb"] = round(total_bytes / 1024 / 1024, 2) + benchmark.extra_info["event_count"] = count + + @pytest.mark.benchmark(group="event_memory_scan_simulation") + def test_event_memory_large_scan(self, benchmark): + """Simulate a large scan (~50K events) and measure per-event memory.""" + count = 50_000 + + benchmark.pedantic(lambda: self._generate_scan_events(count), iterations=1, rounds=3) + + events, total_bytes, per_event = self._measure_memory(count) + benchmark.extra_info["total_memory_bytes"] = total_bytes + benchmark.extra_info["per_event_bytes"] = round(per_event, 1) + benchmark.extra_info["total_memory_mb"] = round(total_bytes / 1024 / 1024, 2) + benchmark.extra_info["event_count"] = count diff --git a/bbot/test/benchmarks/test_excavate_benchmarks.py b/bbot/test/benchmarks/test_excavate_benchmarks.py index bbf2f04b4a..8dbeb82fe4 100644 --- a/bbot/test/benchmarks/test_excavate_benchmarks.py +++ b/bbot/test/benchmarks/test_excavate_benchmarks.py @@ -1,7 +1,11 @@ +import importlib.util + import pytest import asyncio from bbot.scanner import Scanner +HTTP_MODULE = "httpx" if importlib.util.find_spec("bbot.modules.httpx") else "http" + class TestExcavateDirectBenchmarks: """ @@ -99,7 +103,7 @@ def _generate_realistic_content(self, index): async def _run_excavate_single_thread(self, text_segments): """Run excavate processing in single thread""" # Create scanner and initialize excavate - scan = Scanner("example.com", modules=["httpx"], config={"excavate": True}) + scan = Scanner("example.com", modules=[HTTP_MODULE], config={"excavate": True}) await scan._prep() excavate_module = scan.modules.get("excavate") @@ -140,7 +144,7 @@ async def track_emit_event(event_data, *args, **kwargs): async def _run_excavate_parallel_tasks(self, text_segments): """Run excavate processing with parallel asyncio tasks""" # Create scanner and initialize excavate - scan = Scanner("example.com", modules=["httpx"], config={"excavate": True}) + scan = Scanner("example.com", modules=[HTTP_MODULE], config={"excavate": True}) await scan._prep() excavate_module = scan.modules.get("excavate") diff --git a/bbot/test/benchmarks/test_scan_memory.py b/bbot/test/benchmarks/test_scan_memory.py new file mode 100644 index 0000000000..af4f76dfa6 --- /dev/null +++ b/bbot/test/benchmarks/test_scan_memory.py @@ -0,0 +1,66 @@ +""" +Memory benchmarks for BBOT scan patterns. + +Each benchmark launches a scan as a subprocess so tracemalloc measurements +are not contaminated by pytest's own allocations. The subprocess writes +peak memory (MB) to stdout, which the test reads and stores in +benchmark extra_info["total_memory_mb"]. +""" + +import subprocess +import sys +from pathlib import Path + +import pytest + + +NUM_PAGES = 500 +BODY_SIZE = 500_000 # 500 KB per page +SUBDOMAIN_ENUM_COUNT = 5000 + +_BENCHMARKS_DIR = Path(__file__).parent + + +def _run_scan_subprocess(script_path: Path, *args: str) -> float: + """Run a scan script in a clean subprocess, return peak memory in MB.""" + result = subprocess.run( + [sys.executable, str(script_path), *args], + capture_output=True, + text=True, + timeout=600, + ) + if result.returncode != 0: + raise RuntimeError(f"Scan subprocess failed:\n{result.stderr[-2000:]}") + for line in result.stdout.strip().splitlines(): + if line.startswith("PEAK_MB:"): + return float(line.split(":", 1)[1]) + raise RuntimeError(f"No PEAK_MB in subprocess output:\n{result.stdout[-2000:]}") + + +class TestWebCrawlMemory: + """Measures peak memory during a realistic web crawl with large pages.""" + + @pytest.mark.benchmark(group="memory_scan_patterns") + def test_memory_use_web_crawl(self, benchmark): + peak_mb = _run_scan_subprocess( + _BENCHMARKS_DIR / "_scan_memory_web_crawl.py", + str(NUM_PAGES), + str(BODY_SIZE), + ) + benchmark.extra_info["total_memory_mb"] = peak_mb + benchmark.extra_info["num_pages"] = NUM_PAGES + benchmark.pedantic(lambda: None, iterations=1, rounds=1, warmup_rounds=0) + + +class TestSubdomainEnumMemory: + """Measures peak memory during a large subdomain enumeration.""" + + @pytest.mark.benchmark(group="memory_scan_patterns") + def test_memory_use_subdomain_enum(self, benchmark): + peak_mb = _run_scan_subprocess( + _BENCHMARKS_DIR / "_scan_memory_subdomain_enum.py", + str(SUBDOMAIN_ENUM_COUNT), + ) + benchmark.extra_info["total_memory_mb"] = peak_mb + benchmark.extra_info["num_subdomains"] = SUBDOMAIN_ENUM_COUNT + benchmark.pedantic(lambda: None, iterations=1, rounds=1, warmup_rounds=0) diff --git a/bbot/test/benchmarks/test_scan_throughput_benchmarks.py b/bbot/test/benchmarks/test_scan_throughput_benchmarks.py new file mode 100644 index 0000000000..eaf1c5c3b9 --- /dev/null +++ b/bbot/test/benchmarks/test_scan_throughput_benchmarks.py @@ -0,0 +1,91 @@ +""" +Scan throughput benchmark — measures end-to-end HTTP probing performance. + +Runs a real BBOT scan against a local httpserver, measuring how many +HTTP_RESPONSE events are produced per second through the full scan pipeline. + +Automatically detects the available HTTP scan module (httpx or http/blasthttp). + +Run with: + pytest bbot/test/benchmarks/test_scan_throughput_benchmarks.py -v --benchmark-only +""" + +import asyncio +import importlib.util +from threading import Thread +from http.server import HTTPServer, BaseHTTPRequestHandler + +import pytest + + +BENCH_PORT = 18899 + +# On 3.0: scan module is "httpx", output module is "http" +# On blasthttp branch: scan module is "http", output module is "webhook" +HTTP_MODULE = "httpx" if importlib.util.find_spec("bbot.modules.httpx") else "http" + + +class _BenchHandler(BaseHTTPRequestHandler): + """Minimal HTTP handler — returns 200 with a small HTML body for all paths.""" + + def do_GET(self): + body = f"{self.path}ok".encode() + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *args): + pass + + +@pytest.fixture(scope="module", autouse=True) +def bench_httpserver(): + server = HTTPServer(("127.0.0.1", BENCH_PORT), _BenchHandler) + t = Thread(target=server.serve_forever, daemon=True) + t.start() + yield server + server.shutdown() + + +def _run_scan(num_urls): + """Run a scan with num_urls directory targets, return (http_responses, total_events).""" + from bbot.scanner import Scanner + + targets = [f"http://127.0.0.1:{BENCH_PORT}/dir{i}/" for i in range(num_urls)] + config = { + "web": {"http_timeout": 10, "http_retries": 0, "ssl_verify": False}, + "scope": {"search_distance": 0, "report_distance": 0}, + "modules": {"speculate": {"ports": str(BENCH_PORT)}}, + "excavate": False, + "aggregate": False, + "cloudcheck": False, + "omit_event_types": [], + } + + scan = Scanner(*targets, modules=[HTTP_MODULE], config=config) + event_counts = {} + + async def _inner(): + async for event in scan.async_start(): + event_counts[event.type] = event_counts.get(event.type, 0) + 1 + + asyncio.run(_inner()) + return event_counts.get("HTTP_RESPONSE", 0), sum(event_counts.values()) + + +class TestScanThroughputBenchmarks: + """Benchmark full scan pipeline throughput.""" + + def test_scan_throughput_100(self, benchmark): + """100 target URLs through the full scan pipeline.""" + result = benchmark.pedantic(_run_scan, args=(100,), rounds=3, warmup_rounds=1) + http_responses, total = result + assert http_responses >= 100, f"Expected at least 100 HTTP_RESPONSE events, got {http_responses}" + + def test_scan_throughput_1000(self, benchmark): + """1000 target URLs through the full scan pipeline.""" + result = benchmark.pedantic(_run_scan, args=(1000,), rounds=3, warmup_rounds=1) + http_responses, total = result + assert http_responses >= 1000, f"Expected at least 1000 HTTP_RESPONSE events, got {http_responses}" diff --git a/bbot/test/conftest.py b/bbot/test/conftest.py index 2b39ae2e2b..ac4ef7b8b2 100644 --- a/bbot/test/conftest.py +++ b/bbot/test/conftest.py @@ -94,7 +94,10 @@ def bbot_httpserver_ssl(): def should_mock(request): - return request.url.host not in ["127.0.0.1", "localhost", "raw.githubusercontent.com"] + interactsh_servers + return ( + request.url.host + not in ["127.0.0.1", "localhost", "raw.githubusercontent.com", "asndb.api.bbot.io"] + interactsh_servers + ) def pytest_collection_modifyitems(config, items): @@ -343,6 +346,25 @@ def pytest_sessionfinish(session, exitstatus): # Wipe out BBOT home dir shutil.rmtree("/tmp/.bbot_test", ignore_errors=True) + # Ensure stdout/stderr are blocking before pytest writes summaries + try: + import sys + import fcntl + import os + import io + + fds = [] + for stream in (sys.stdout, sys.stderr): + try: + fds.append(stream.fileno()) + except io.UnsupportedOperation: + pass + for fd in fds: + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + except Exception: + pass + yield # temporarily suspend stdout capture and print detailed thread info diff --git a/bbot/test/fastapi_test.py b/bbot/test/fastapi_test.py index f0c7b2d789..a4a1d57107 100644 --- a/bbot/test/fastapi_test.py +++ b/bbot/test/fastapi_test.py @@ -1,5 +1,5 @@ from typing import List -from bbot import Scanner +from bbot.scanner import Scanner from fastapi import FastAPI, Query app = FastAPI() diff --git a/bbot/test/test_step_1/test__module__tests.py b/bbot/test/test_step_1/test__module__tests.py index 6221b61490..b68ad50a5d 100644 --- a/bbot/test/test_step_1/test__module__tests.py +++ b/bbot/test/test_step_1/test__module__tests.py @@ -2,7 +2,7 @@ import importlib from pathlib import Path -from bbot import Preset +from bbot.scanner import Preset from ..test_step_2.module_tests.base import ModuleTestBase log = logging.getLogger("bbot.test.modules") diff --git a/bbot/test/test_step_1/test_bbot_fastapi.py b/bbot/test/test_step_1/test_bbot_fastapi.py index 669ca827d9..3dca8aeded 100644 --- a/bbot/test/test_step_1/test_bbot_fastapi.py +++ b/bbot/test/test_step_1/test_bbot_fastapi.py @@ -9,7 +9,7 @@ def run_bbot_multiprocess(queue): - from bbot import Scanner + from bbot.scanner import Scanner scan = Scanner("http://127.0.0.1:8888", "blacklanternsecurity.com", modules=["httpx"]) events = [e.json() for e in scan.start()] @@ -27,7 +27,7 @@ def test_bbot_multiprocess(bbot_httpserver): assert len(events) >= 3 scan_events = [e for e in events if e["type"] == "SCAN"] assert len(scan_events) == 2 - assert any(e["data"] == "test@blacklanternsecurity.com" for e in events) + assert any(e.get("data", "") == "test@blacklanternsecurity.com" for e in events) def test_bbot_fastapi(bbot_httpserver): @@ -58,7 +58,7 @@ def test_bbot_fastapi(bbot_httpserver): assert len(events) >= 3 scan_events = [e for e in events if e["type"] == "SCAN"] assert len(scan_events) == 2 - assert any(e["data"] == "test@blacklanternsecurity.com" for e in events) + assert any(e.get("data", "") == "test@blacklanternsecurity.com" for e in events) finally: with suppress(Exception): diff --git a/bbot/test/test_step_1/test_bloom_filter.py b/bbot/test/test_step_1/test_bloom_filter.py index 0a43f34157..73e1be0094 100644 --- a/bbot/test/test_step_1/test_bloom_filter.py +++ b/bbot/test/test_step_1/test_bloom_filter.py @@ -13,6 +13,7 @@ def generate_random_strings(n, length=10): from bbot.scanner import Scanner scan = Scanner() + await scan._prep() n_items_to_add = 100000 n_items_to_test = 100000 diff --git a/bbot/test/test_step_1/test_cli.py b/bbot/test/test_step_1/test_cli.py index 9d95870a0b..9824bd3fee 100644 --- a/bbot/test/test_step_1/test_cli.py +++ b/bbot/test/test_step_1/test_cli.py @@ -12,10 +12,10 @@ async def test_cli_scope(monkeypatch, capsys): monkeypatch.setattr(sys, "exit", lambda *args, **kwargs: True) monkeypatch.setattr(os, "_exit", lambda *args, **kwargs: True) - # basic target without whitelist + # basic target (seeds and target are the same) monkeypatch.setattr( "sys.argv", - ["bbot", "-t", "one.one.one.one", "-c", "scope.report_distance=10", "dns.minimal=false", "--json"], + ["bbot", "-t", "one.one.one.one", "-c", "scope.report_distance=10", "dns.minimal=false", "--json", "-y"], ) result = await cli._main() out, err = capsys.readouterr() @@ -28,10 +28,7 @@ async def test_cli_scope(monkeypatch, capsys): [ l for l in dns_events - if l["module"] == "TARGET" - and l["scope_distance"] == 0 - and "in-scope" in l["tags"] - and "target" in l["tags"] + if l["module"] == "SEED" and l["scope_distance"] == 0 and "in-scope" in l["tags"] and "seed" in l["tags"] ] ) ip_events = [l for l in lines if l["type"] == "IP_ADDRESS" and l["data"] == "1.1.1.1"] @@ -41,20 +38,21 @@ async def test_cli_scope(monkeypatch, capsys): assert ip_events assert all(l["scope_distance"] == 1 and "distance-1" in l["tags"] for l in ip_events) - # with whitelist + # with target_list different from seeds (seeds are one.one.one.one, target is 192.168.0.1) monkeypatch.setattr( "sys.argv", [ "bbot", "-t", - "one.one.one.one", - "-w", "192.168.0.1", + "-s", + "one.one.one.one", "-c", "scope.report_distance=10", "dns.minimal=false", "dns.search_distance=2", "--json", + "-y", ], ) result = await cli._main() @@ -66,17 +64,17 @@ async def test_cli_scope(monkeypatch, capsys): assert not any(l["scope_distance"] == 0 for l in lines) dns_events = [l for l in lines if l["type"] == "DNS_NAME" and l["data"] == "one.one.one.one"] assert dns_events + # When seeds are different from target, the seed DNS_NAME should be out-of-scope + # (distance-1) and tagged as a seed, but NOT tagged as a target (since it is not + # part of the target set that in_target() checks). assert all(l["scope_distance"] == 1 and "distance-1" in l["tags"] for l in dns_events) - assert 1 == len( - [ - l - for l in dns_events - if l["module"] == "TARGET" - and l["scope_distance"] == 1 - and "distance-1" in l["tags"] - and "target" in l["tags"] - ] - ) + target_seed_events = [ + l + for l in dns_events + if l["module"] == "SEED" and l["scope_distance"] == 1 and "distance-1" in l["tags"] and "seed" in l["tags"] + ] + assert len(target_seed_events) == 1 + assert all("target" not in l["tags"] for l in target_seed_events) ip_events = [l for l in lines if l["type"] == "IP_ADDRESS" and l["data"] == "1.1.1.1"] assert ip_events assert all(l["scope_distance"] == 2 and "distance-2" in l["tags"] for l in ip_events) @@ -123,9 +121,9 @@ async def test_cli_scan(monkeypatch): with open(output_filename) as f: lines = f.read().splitlines() for line in lines: - if "[IP_ADDRESS] \t127.0.0.1\tTARGET" in line: + if "[IP_ADDRESS] \t127.0.0.1\tSEED" in line: ip_success = True - if "[DNS_NAME] \twww.example.com\tTARGET" in line: + if "[DNS_NAME] \twww.example.com\tSEED" in line: dns_success = True assert ip_success and dns_success, "IP_ADDRESS and/or DNS_NAME are not present in output.txt" @@ -158,11 +156,11 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): print(out) # parse YAML output preset = yaml.safe_load(out) - assert preset == { - "description": "depstest", - "scan_name": "depstest", - "config": {"deps": {"behavior": "retry_failed"}}, - } + # description and scan_name should reflect the CLI name + assert preset["description"] == "depstest" + assert preset["scan_name"] == "depstest" + # deps behavior should be set to retry_failed, but allow other config keys to exist + assert preset.get("config", {}).get("deps") == {"behavior": "retry_failed"} # list modules monkeypatch.setattr("sys.argv", ["bbot", "--list-modules"]) @@ -253,8 +251,7 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): result = await cli._main() out, err = capsys.readouterr() assert result is None - assert "| safe " in out - assert "| Non-intrusive, safe to run " in out + assert "| loud " in out assert "| active " in out assert "| passive " in out @@ -268,11 +265,11 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): assert "| passive " not in out # list multiple flags - monkeypatch.setattr("sys.argv", ["bbot", "-f", "active", "safe", "--list-flags"]) + monkeypatch.setattr("sys.argv", ["bbot", "-f", "active", "loud", "--list-flags"]) result = await cli._main() out, err = capsys.readouterr() assert result is None - assert "| safe " in out + assert "| loud " in out assert "| active " in out assert "| passive " not in out @@ -370,7 +367,7 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): result = await cli._main() out, err = capsys.readouterr() assert result is True - assert "[ORG_STUB] evilcorp TARGET" in out + assert "[ORG_STUB] evilcorp\tSEED" in out # activate modules by flag caplog.clear() @@ -404,18 +401,10 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): result = await cli._main() assert result is True - # deadly modules - caplog.clear() - assert not caplog.text + # invasive modules should run without a gate (just warnings) monkeypatch.setattr("sys.argv", ["bbot", "-m", "nuclei"]) result = await cli._main() - assert result is False, "-m nuclei ran without --allow-deadly" - assert "Please specify --allow-deadly to continue" in caplog.text - - # --allow-deadly - monkeypatch.setattr("sys.argv", ["bbot", "-m", "nuclei", "--allow-deadly"]) - result = await cli._main() - assert result is True, "-m nuclei failed to run with --allow-deadly" + assert result is True, "-m nuclei should run without any special flags" # install all deps monkeypatch.setattr("sys.argv", ["bbot", "--install-all-deps"]) @@ -618,7 +607,7 @@ def test_cli_module_validation(monkeypatch, caplog): assert 'Did you mean "subdomain-enum"?' in caplog.text -def test_cli_presets(monkeypatch, capsys, caplog): +def test_cli_presets(monkeypatch, capsys, caplog, clean_default_config): import yaml monkeypatch.setattr(sys, "exit", lambda *args, **kwargs: True) diff --git a/bbot/test/test_step_1/test_command.py b/bbot/test/test_step_1/test_command.py index 7a99aed9bc..54bbdaba25 100644 --- a/bbot/test/test_step_1/test_command.py +++ b/bbot/test/test_step_1/test_command.py @@ -6,6 +6,7 @@ @pytest.mark.asyncio async def test_command(bbot_scanner): scan1 = bbot_scanner() + await scan1._prep() # test timeouts command = ["sleep", "3"] @@ -116,7 +117,7 @@ async def test_command(bbot_scanner): assert not lines # test sudo + existence of environment variables - await scan1.load_modules() + await scan1._prep() path_parts = os.environ.get("PATH", "").split(":") assert "/tmp/.bbot_test/tools" in path_parts run_lines = (await scan1.helpers.run(["env"])).stdout.splitlines() diff --git a/bbot/test/test_step_1/test_config.py b/bbot/test/test_step_1/test_config.py index 72f7961379..b040bf5dc0 100644 --- a/bbot/test/test_step_1/test_config.py +++ b/bbot/test/test_step_1/test_config.py @@ -15,7 +15,7 @@ async def test_config(bbot_scanner): } ) scan1 = bbot_scanner("127.0.0.1", modules=["ipneighbor"], config=config) - await scan1.load_modules() + await scan1._prep() assert scan1.config.web.user_agent == "BBOT Test User-Agent" assert scan1.config.plumbus == "asdf" assert scan1.modules["ipneighbor"].config.test_option == "ipneighbor" diff --git a/bbot/test/test_step_1/test_db_models.py b/bbot/test/test_step_1/test_db_models.py new file mode 100644 index 0000000000..8c926b84da --- /dev/null +++ b/bbot/test/test_step_1/test_db_models.py @@ -0,0 +1,93 @@ +from datetime import datetime +from zoneinfo import ZoneInfo + +from bbot.models.pydantic import Event +from bbot.core.event.base import BaseEvent +from bbot.models.helpers import utc_datetime_validator +from ..bbot_fixtures import * # noqa + + +def test_pydantic_models(events, bbot_scanner): + # test datetime helpers + now = datetime.now(ZoneInfo("America/New_York")) + utc_now = utc_datetime_validator(now) + assert now.timestamp() == utc_now.timestamp() + now2 = datetime.fromtimestamp(utc_now.timestamp(), ZoneInfo("UTC")) + assert now2.timestamp() == utc_now.timestamp() + utc_now2 = utc_datetime_validator(now2) + assert utc_now2.timestamp() == utc_now.timestamp() + + test_event = Event(**events.ipv4.json()) + assert sorted(test_event.indexed_fields()) == [ + "data", + "host", + "id", + "inserted_at", + "module", + "parent", + "parent_uuid", + "reverse_host", + "scan", + "timestamp", + "type", + "uuid", + ] + + # convert events to pydantic and back, making sure they're exactly the same + for event in ("ipv4", "http_response", "finding", "storage_bucket"): + e = getattr(events, event) + event_json = e.json() + event_pydantic = Event(**event_json) + event_pydantic_dict = event_pydantic.model_dump() + event_reconstituted = BaseEvent.from_json(event_pydantic.model_dump(exclude_none=True)) + assert isinstance(event_json["timestamp"], float) + assert isinstance(e.timestamp, datetime) + assert isinstance(event_pydantic.timestamp, float) + assert not "inserted_at" in event_json + assert isinstance(event_pydantic_dict["timestamp"], float) + assert isinstance(event_pydantic_dict["inserted_at"], float) + + event_pydantic_dict = event_pydantic.model_dump( + exclude_none=True, exclude=["reverse_host", "inserted_at", "archived"] + ) + assert event_pydantic_dict == event_json + event_pydantic_dict.pop("scan") + event_pydantic_dict.pop("module") + event_pydantic_dict.pop("module_sequence") + assert event_reconstituted.json() == event_pydantic_dict + + # make sure we can dedupe events by their id + scan = bbot_scanner() + event1 = scan.make_event("1.2.3.4", parent=scan.root_event) + event2 = scan.make_event("1.2.3.4", parent=scan.root_event) + event3 = scan.make_event("evilcorp.com", parent=scan.root_event) + event4 = scan.make_event("evilcorp.com", parent=scan.root_event) + # first two events are IPS + assert event1.uuid != event2.uuid + assert event1.id == event2.id + # second two are DNS + assert event2.uuid != event3.uuid + assert event2.id != event3.id + assert event3.uuid != event4.uuid + assert event3.id == event4.id + + event_set_bbot = { + event1, + event2, + event3, + event4, + } + assert len(event_set_bbot) == 2 + assert set([e.type for e in event_set_bbot]) == {"IP_ADDRESS", "DNS_NAME"} + + event_set_pydantic = { + Event(**event1.json()), + Event(**event2.json()), + Event(**event3.json()), + Event(**event4.json()), + } + assert len(event_set_pydantic) == 2 + assert set([e.type for e in event_set_pydantic]) == {"IP_ADDRESS", "DNS_NAME"} + + +# TODO: SQL diff --git a/bbot/test/test_step_1/test_depsinstaller.py b/bbot/test/test_step_1/test_depsinstaller.py index 9dff1c0281..76a363ae5d 100644 --- a/bbot/test/test_step_1/test_depsinstaller.py +++ b/bbot/test/test_step_1/test_depsinstaller.py @@ -6,6 +6,7 @@ async def test_depsinstaller(monkeypatch, bbot_scanner): scan = bbot_scanner( "127.0.0.1", ) + await scan._prep() # test shell test_file = Path("/tmp/test_file") diff --git a/bbot/test/test_step_1/test_dns.py b/bbot/test/test_step_1/test_dns.py index a8bfefa3a1..9bb89c7070 100644 --- a/bbot/test/test_step_1/test_dns.py +++ b/bbot/test/test_step_1/test_dns.py @@ -18,6 +18,7 @@ @pytest.mark.asyncio async def test_dns_engine(bbot_scanner): scan = bbot_scanner() + await scan._prep() await scan.helpers._mock_dns( {"one.one.one.one": {"A": ["1.1.1.1"]}, "1.1.1.1.in-addr.arpa": {"PTR": ["one.one.one.one"]}} ) @@ -168,6 +169,7 @@ async def test_dns_resolution(bbot_scanner): assert "a-record" not in resolved_hosts_event2.tags scan2 = bbot_scanner("evilcorp.com", config={"dns": {"minimal": False}}) + await scan2._prep() await scan2.helpers.dns._mock_dns( { "evilcorp.com": {"TXT": ['"v=spf1 include:cloudprovider.com ~all"']}, @@ -186,6 +188,7 @@ async def test_dns_resolution(bbot_scanner): @pytest.mark.asyncio async def test_wildcards(bbot_scanner): scan = bbot_scanner("1.1.1.1") + await scan._prep() helpers = scan.helpers from bbot.core.helpers.dns.engine import DNSEngine, all_rdtypes @@ -253,16 +256,18 @@ def custom_lookup(query, rdtype): # first, we check with wildcard detection disabled scan = bbot_scanner( - "bbot.fdsa.www.test.evilcorp.com", - whitelist=["evilcorp.com"], + "evilcorp.com", + seeds=["bbot.fdsa.www.test.evilcorp.com"], config={ "dns": {"minimal": False, "disable": False, "search_distance": 5, "wildcard_ignore": ["evilcorp.com"]}, "speculate": True, }, ) + await scan._prep() await scan.helpers.dns._mock_dns(mock_data, custom_lookup_fn=custom_lookup) events = [e async for e in scan.async_start()] + assert len(events) == 12 assert len([e for e in events if e.type == "DNS_NAME"]) == 5 assert len([e for e in events if e.type == "RAW_DNS_RECORD"]) == 4 @@ -275,7 +280,12 @@ def custom_lookup(query, rdtype): ] dns_names_by_host = {e.host: e for e in events if e.type == "DNS_NAME"} - assert dns_names_by_host["evilcorp.com"].tags == {"domain", "private-ip", "in-scope", "a-record"} + assert dns_names_by_host["evilcorp.com"].tags == { + "domain", + "private-ip", + "in-scope", + "a-record", + } assert dns_names_by_host["evilcorp.com"].resolved_hosts == {"127.0.0.1"} assert dns_names_by_host["test.evilcorp.com"].tags == { "subdomain", @@ -294,6 +304,7 @@ def custom_lookup(query, rdtype): "subdomain", "in-scope", "txt-record", + "seed", } assert dns_names_by_host["bbot.fdsa.www.test.evilcorp.com"].resolved_hosts == set() @@ -310,16 +321,18 @@ def custom_lookup(query, rdtype): # then we run it again with wildcard detection enabled scan = bbot_scanner( - "bbot.fdsa.www.test.evilcorp.com", - whitelist=["evilcorp.com"], + "evilcorp.com", + seeds=["bbot.fdsa.www.test.evilcorp.com"], config={ "dns": {"minimal": False, "disable": False, "search_distance": 5, "wildcard_ignore": []}, "speculate": True, }, ) + await scan._prep() await scan.helpers.dns._mock_dns(mock_data, custom_lookup_fn=custom_lookup) events = [e async for e in scan.async_start()] + assert len(events) == 12 assert len([e for e in events if e.type == "DNS_NAME"]) == 5 assert len([e for e in events if e.type == "RAW_DNS_RECORD"]) == 4 @@ -332,7 +345,12 @@ def custom_lookup(query, rdtype): ] dns_names_by_host = {e.host: e for e in events if e.type == "DNS_NAME"} - assert dns_names_by_host["evilcorp.com"].tags == {"domain", "private-ip", "in-scope", "a-record"} + assert dns_names_by_host["evilcorp.com"].tags == { + "domain", + "private-ip", + "in-scope", + "a-record", + } assert dns_names_by_host["evilcorp.com"].resolved_hosts == {"127.0.0.1"} assert dns_names_by_host["test.evilcorp.com"].tags == { "subdomain", @@ -366,6 +384,7 @@ def custom_lookup(query, rdtype): "txt-record", "txt-wildcard", "wildcard", + "seed", } assert dns_names_by_host["bbot.fdsa.www.test.evilcorp.com"].resolved_hosts == set() @@ -414,6 +433,7 @@ def custom_lookup(query, rdtype): }, }, ) + await scan._prep() await scan.helpers.dns._mock_dns(mock_data, custom_lookup_fn=custom_lookup) events = [e async for e in scan.async_start()] @@ -437,6 +457,7 @@ def custom_lookup(query, rdtype): "domain", "srv-record", "private-ip", + "seed", } assert dns_names_by_host["test.evilcorp.com"].tags == { "in-scope", @@ -491,6 +512,7 @@ def custom_lookup(query, rdtype): } scan = bbot_scanner("1.1.1.1") + await scan._prep() helpers = scan.helpers # event resolution @@ -539,13 +561,19 @@ def custom_lookup(query, rdtype): from bbot.scanner import Scanner # test with full scan - scan2 = Scanner("asdfl.gashdgkjsadgsdf.github.io", whitelist=["github.io"], config={"dns": {"minimal": False}}) + + scan2 = Scanner( + "github.io", + seeds=["asdfl.gashdgkjsadgsdf.github.io"], + config={"dns": {"minimal": False}}, + ) await scan2._prep() other_event = scan2.make_event( "lkjg.sdfgsg.jgkhajshdsadf.github.io", module=scan2.modules["dnsresolve"], parent=scan2.root_event ) await scan2.ingress_module.queue_event(other_event, {}) events = [e async for e in scan2.async_start()] + assert len(events) == 4 assert 2 == len([e for e in events if e.type == "SCAN"]) unmodified_wildcard_events = [ @@ -581,8 +609,8 @@ def custom_lookup(query, rdtype): # test with full scan (wildcard detection disabled for domain) scan2 = Scanner( - "asdfl.gashdgkjsadgsdf.github.io", - whitelist=["github.io"], + "github.io", + seeds=["asdfl.gashdgkjsadgsdf.github.io"], config={"dns": {"wildcard_ignore": ["github.io"]}}, exclude_modules=["cloudcheck"], ) @@ -657,6 +685,7 @@ async def handle_event(self, event): scan = bbot_scanner( "evilcorp.com", config={"dns": {"minimal": False, "wildcard_ignore": []}, "omit_event_types": []} ) + await scan._prep() await scan.helpers.dns._mock_dns(mock_data, custom_lookup_fn=custom_lookup) dummy_module = DummyModule(scan) scan.modules["dummy_module"] = dummy_module @@ -682,8 +711,10 @@ async def handle_event(self, event): # scan without omitted event type scan = bbot_scanner("one.one.one.one", "1.1.1.1", config={"dns": {"minimal": False}, "omit_event_types": []}) + await scan._prep() await scan.helpers.dns._mock_dns(mock_records) dummy_module = DummyModule(scan) + await dummy_module.setup() scan.modules["dummy_module"] = dummy_module events = [e async for e in scan.async_start()] assert 1 == len([e for e in events if e.type == "RAW_DNS_RECORD"]) @@ -715,8 +746,10 @@ async def handle_event(self, event): ) # scan with omitted event type scan = bbot_scanner("one.one.one.one", config={"dns": {"minimal": False}, "omit_event_types": ["RAW_DNS_RECORD"]}) + await scan._prep() await scan.helpers.dns._mock_dns(mock_records) dummy_module = DummyModule(scan) + await dummy_module.setup() scan.modules["dummy_module"] = dummy_module events = [e async for e in scan.async_start()] # no raw records should be emitted @@ -726,8 +759,10 @@ async def handle_event(self, event): # scan with watching module DummyModule.watched_events = ["RAW_DNS_RECORD"] scan = bbot_scanner("one.one.one.one", config={"dns": {"minimal": False}, "omit_event_types": ["RAW_DNS_RECORD"]}) + await scan._prep() await scan.helpers.dns._mock_dns(mock_records) dummy_module = DummyModule(scan) + await dummy_module.setup() scan.modules["dummy_module"] = dummy_module events = [e async for e in scan.async_start()] # no raw records should be output @@ -751,6 +786,7 @@ async def handle_event(self, event): @pytest.mark.asyncio async def test_dns_graph_structure(bbot_scanner): scan = bbot_scanner("https://evilcorp.com", config={"dns": {"search_distance": 1, "minimal": False}}) + await scan._prep() await scan.helpers.dns._mock_dns( { "evilcorp.com": { @@ -766,19 +802,20 @@ async def test_dns_graph_structure(bbot_scanner): assert len(events) == 6 non_scan_events = [e for e in events if e.type != "SCAN"] assert sorted([e.type for e in non_scan_events]) == ["DNS_NAME", "DNS_NAME", "DNS_NAME", "URL_UNVERIFIED"] - events_by_data = {e.data: e for e in non_scan_events} + events_by_data = {e.pretty_string: e for e in non_scan_events} assert set(events_by_data) == {"https://evilcorp.com/", "evilcorp.com", "www.evilcorp.com", "test.evilcorp.com"} assert events_by_data["test.evilcorp.com"].parent.data == "www.evilcorp.com" assert str(events_by_data["test.evilcorp.com"].module) == "CNAME" assert events_by_data["www.evilcorp.com"].parent.data == "evilcorp.com" assert str(events_by_data["www.evilcorp.com"].module) == "CNAME" - assert events_by_data["evilcorp.com"].parent.data == "https://evilcorp.com/" + assert events_by_data["evilcorp.com"].parent.url == "https://evilcorp.com/" assert str(events_by_data["evilcorp.com"].module) == "host" @pytest.mark.asyncio async def test_hostname_extraction(bbot_scanner): scan = bbot_scanner("evilcorp.com", config={"dns": {"minimal": False}}) + await scan._prep() await scan.helpers.dns._mock_dns( { "evilcorp.com": { @@ -825,6 +862,7 @@ async def test_dns_helpers(bbot_scanner): # make sure system nameservers are excluded from use by DNS brute force brute_nameservers = tempwordlist(["1.2.3.4", "8.8.4.4", "4.3.2.1", "8.8.8.8"]) scan = bbot_scanner(config={"dns": {"brute_nameservers": brute_nameservers}}) + await scan._prep() scan.helpers.dns.system_resolvers = ["8.8.8.8", "8.8.4.4"] resolver_file = await scan.helpers.dns.brute.resolver_file() resolvers = set(scan.helpers.read_file(resolver_file)) diff --git a/bbot/test/test_step_1/test_event_seeds.py b/bbot/test/test_step_1/test_event_seeds.py index 5a6ada3e25..42f812ac3d 100644 --- a/bbot/test/test_step_1/test_event_seeds.py +++ b/bbot/test/test_step_1/test_event_seeds.py @@ -81,7 +81,7 @@ def test_event_seeds(): # URL (DNS_NAME) url_dns_seed = EventSeed("http://evilcOrp.com./index.html?a=b#c") assert url_dns_seed.type == "URL_UNVERIFIED" - assert url_dns_seed.data == "http://evilcorp.com/index.html?a=b" + assert url_dns_seed.url == "http://evilcorp.com/index.html?a=b" assert url_dns_seed.host == "evilcorp.com" assert url_dns_seed.port == 80 assert url_dns_seed.input == "http://evilcorp.com/index.html?a=b" @@ -89,7 +89,7 @@ def test_event_seeds(): # URL (IPv4) url_ipv4_seed = EventSeed("https://192.168.1.1/index.html?a=b#c") assert url_ipv4_seed.type == "URL_UNVERIFIED" - assert url_ipv4_seed.data == "https://192.168.1.1/index.html?a=b" + assert url_ipv4_seed.url == "https://192.168.1.1/index.html?a=b" assert url_ipv4_seed.host == ipaddress.ip_address("192.168.1.1") assert url_ipv4_seed.port == 443 assert url_ipv4_seed.input == "https://192.168.1.1/index.html?a=b" @@ -97,7 +97,7 @@ def test_event_seeds(): # URL (IPv6) url_ipv6_seed = EventSeed("https://[2001:db8::42]:8080/index.html?a=b#c") assert url_ipv6_seed.type == "URL_UNVERIFIED" - assert url_ipv6_seed.data == "https://[2001:db8::42]:8080/index.html?a=b" + assert url_ipv6_seed.url == "https://[2001:db8::42]:8080/index.html?a=b" assert url_ipv6_seed.host == ipaddress.ip_address("2001:db8::42") assert url_ipv6_seed.port == 8080 assert url_ipv6_seed.input == "https://[2001:db8::42]:8080/index.html?a=b" diff --git a/bbot/test/test_step_1/test_events.py b/bbot/test/test_step_1/test_events.py index b4bb8582eb..d1505df88f 100644 --- a/bbot/test/test_step_1/test_events.py +++ b/bbot/test/test_step_1/test_events.py @@ -1,4 +1,3 @@ -import json import random import ipaddress @@ -118,18 +117,18 @@ async def test_events(events, helpers): assert events.emoji not in events.ipv6_url_unverified assert events.url_unverified not in events.emoji - # URL normalization tests – compare against normalized event.data / .with_port().geturl() - assert scan.make_event("https://evilcorp.com:443", dummy=True).data == "https://evilcorp.com/" - assert scan.make_event("http://evilcorp.com:80", dummy=True).data == "http://evilcorp.com/" + # URL normalization tests – compare against normalized event.url / .with_port().geturl() + assert scan.make_event("https://evilcorp.com:443", dummy=True).url == "https://evilcorp.com/" + assert scan.make_event("http://evilcorp.com:80", dummy=True).url == "http://evilcorp.com/" assert "http://evilcorp.com:80/asdf.js" in scan.make_event("http://evilcorp.com/asdf.js", dummy=True) assert "http://evilcorp.com/asdf.js" in scan.make_event("http://evilcorp.com:80/asdf.js", dummy=True) - assert scan.make_event("https://evilcorp.com", dummy=True).data == "https://evilcorp.com/" - assert scan.make_event("http://evilcorp.com", dummy=True).data == "http://evilcorp.com/" - assert scan.make_event("https://evilcorp.com:80", dummy=True).data == "https://evilcorp.com:80/" - assert scan.make_event("http://evilcorp.com:443", dummy=True).data == "http://evilcorp.com:443/" + assert scan.make_event("https://evilcorp.com", dummy=True).url == "https://evilcorp.com/" + assert scan.make_event("http://evilcorp.com", dummy=True).url == "http://evilcorp.com/" + assert scan.make_event("https://evilcorp.com:80", dummy=True).url == "https://evilcorp.com:80/" + assert scan.make_event("http://evilcorp.com:443", dummy=True).url == "http://evilcorp.com:443/" assert scan.make_event("https://evilcorp.com", dummy=True).with_port().geturl() == "https://evilcorp.com:443/" assert scan.make_event("https://evilcorp.com:666", dummy=True).with_port().geturl() == "https://evilcorp.com:666/" - assert scan.make_event("https://evilcorp.com.:666", dummy=True).data == "https://evilcorp.com:666/" + assert scan.make_event("https://evilcorp.com.:666", dummy=True).url == "https://evilcorp.com:666/" assert scan.make_event("https://[bad::c0de]", dummy=True).with_port().geturl() == "https://[bad::c0de]:443/" assert scan.make_event("https://[bad::c0de]:666", dummy=True).with_port().geturl() == "https://[bad::c0de]:666/" url_event = scan.make_event("https://evilcorp.com", "URL", events.ipv4_url, tags=["status-200"]) @@ -336,26 +335,112 @@ async def test_events(events, helpers): assert "affiliate" in corrected_event4.tags test_vuln = scan.make_event( - {"host": "EVILcorp.com", "severity": "iNfo ", "description": "asdf"}, "VULNERABILITY", dummy=True + { + "host": "EVILcorp.com", + "severity": "iNfo ", + "confidence": "HIGH", + "description": "asdf", + "name": "Test Finding", + }, + "FINDING", + dummy=True, ) assert test_vuln.data["host"] == "evilcorp.com" assert test_vuln.data["severity"] == "INFO" test_vuln2 = scan.make_event( - {"host": "192.168.1.1", "severity": "iNfo ", "description": "asdf"}, "VULNERABILITY", dummy=True + { + "host": "192.168.1.1", + "severity": "INFO", + "confidence": "HIGH", + "description": "asdf", + "name": "Vulnerability", + }, + "FINDING", + dummy=True, ) - assert json.loads(test_vuln2.data_human)["severity"] == "INFO" + assert test_vuln2.data_human == "Severity: [INFO] Confidence: [HIGH] asdf" assert test_vuln2.host.is_private + # must have severity with pytest.raises(ValidationError, match=".*validation error.*\nseverity\n.*Field required.*"): - test_vuln = scan.make_event({"host": "evilcorp.com", "description": "asdf"}, "VULNERABILITY", dummy=True) + test_vuln = scan.make_event({"host": "evilcorp.com", "description": "asdf"}, "FINDING", dummy=True) with pytest.raises(ValidationError, match=".*host.*\n.*Invalid host.*"): test_vuln = scan.make_event( - {"host": "!@#$", "severity": "INFO", "description": "asdf"}, "VULNERABILITY", dummy=True + {"host": "!@#$", "severity": "INFO", "confidence": "HIGH", "description": "asdf"}, + "FINDING", + dummy=True, ) + # invalid severity with pytest.raises(ValidationError, match=".*severity.*\n.*Invalid severity.*"): test_vuln = scan.make_event( - {"host": "evilcorp.com", "severity": "WACK", "description": "asdf"}, "VULNERABILITY", dummy=True + {"host": "evilcorp.com", "severity": "WACK", "confidence": "HIGH", "description": "asdf"}, + "FINDING", + dummy=True, + ) + # invalid confidence + with pytest.raises(ValidationError, match=".*confidence.*\n.*Invalid confidence.*"): + test_vuln = scan.make_event( + { + "host": "evilcorp.com", + "severity": "HIGH", + "confidence": "INVALID", + "description": "asdf", + "name": "Test", + }, + "FINDING", + dummy=True, + ) + # must have confidence + with pytest.raises(ValidationError, match=".*confidence.*\n.*Field required.*"): + test_vuln = scan.make_event( + {"host": "evilcorp.com", "severity": "HIGH", "description": "asdf", "name": "Test"}, + "FINDING", + dummy=True, ) + # test confidence colors and formatting + from bbot.core.event.base import FINDING + + expected_colors = {"CONFIRMED": "🟣", "HIGH": "🔴", "MEDIUM": "🟠", "LOW": "🟡", "UNKNOWN": "⚪"} + assert FINDING.confidence_colors == expected_colors + + # test CONFIRMED gets bold formatting + confirmed_finding = scan.make_event( + { + "host": "test.com", + "name": "Test", + "description": "Test", + "severity": "HIGH", + "confidence": "CONFIRMED", + "url": "http://test.com", + }, + "FINDING", + dummy=True, + ) + assert confirmed_finding.host == "test.com" + assert confirmed_finding.port == 80 + assert confirmed_finding.netloc == "test.com:80" + assert confirmed_finding.parsed_url.geturl() == "http://test.com/" + pretty_string = confirmed_finding._pretty_string() + assert "[\033[1mCONFIRMED\033[0m]" in pretty_string + assert f"confidence-{confirmed_finding.data['confidence'].lower()}" in confirmed_finding.tags + + # must have name + with pytest.raises(ValidationError, match=".*name.*\n.*Field required.*"): + test_vuln = scan.make_event( + {"host": "evilcorp.com", "severity": "INFO", "description": "asdf", "confidence": "HIGH"}, + "FINDING", + dummy=True, + ) + + # technology should be lowercased + tech_event = scan.make_event( + {"host": "evilcorp.com", "technology": "HTTP", "url": "http://evilcorp.com/test"}, + "TECHNOLOGY", + dummy=True, + ) + assert tech_event.data["technology"] == "http" + assert tech_event.port == 80 + # test tagging ip_event_1 = scan.make_event("8.8.8.8", dummy=True) assert "private-ip" not in ip_event_1.tags @@ -405,9 +490,9 @@ async def test_events(events, helpers): assert scan.make_event("bob@xn--eckwd4c7c.xn--zckzah", dummy=True).data == "bob@xn--eckwd4c7c.xn--zckzah" assert scan.make_event("テスト@xn--eckwd4c7c.xn--zckzah", dummy=True).data == "テスト@xn--eckwd4c7c.xn--zckzah" assert scan.make_event("xn--eckwd4c7c.xn--zckzah:80", dummy=True).data == "xn--eckwd4c7c.xn--zckzah:80" - assert scan.make_event("http://xn--eckwd4c7c.xn--zckzah:80", dummy=True).data == "http://xn--eckwd4c7c.xn--zckzah/" + assert scan.make_event("http://xn--eckwd4c7c.xn--zckzah:80", dummy=True).url == "http://xn--eckwd4c7c.xn--zckzah/" assert ( - scan.make_event("http://xn--eckwd4c7c.xn--zckzah:80/テスト", dummy=True).data + scan.make_event("http://xn--eckwd4c7c.xn--zckzah:80/テスト", dummy=True).url == "http://xn--eckwd4c7c.xn--zckzah/テスト" ) @@ -415,10 +500,9 @@ async def test_events(events, helpers): assert scan.make_event("bob@ドメイン.テスト", dummy=True).data == "bob@xn--eckwd4c7c.xn--zckzah" assert scan.make_event("テスト@ドメイン.テスト", dummy=True).data == "テスト@xn--eckwd4c7c.xn--zckzah" assert scan.make_event("ドメイン.テスト:80", dummy=True).data == "xn--eckwd4c7c.xn--zckzah:80" - assert scan.make_event("http://ドメイン.テスト:80", dummy=True).data == "http://xn--eckwd4c7c.xn--zckzah/" + assert scan.make_event("http://ドメイン.テスト:80", dummy=True).url == "http://xn--eckwd4c7c.xn--zckzah/" assert ( - scan.make_event("http://ドメイン.テスト:80/テスト", dummy=True).data - == "http://xn--eckwd4c7c.xn--zckzah/テスト" + scan.make_event("http://ドメイン.テスト:80/テスト", dummy=True).url == "http://xn--eckwd4c7c.xn--zckzah/テスト" ) # thai assert ( @@ -437,11 +521,11 @@ async def test_events(events, helpers): == "xn--12c1bik6bbd8ab6hd1b5jc6jta.com:80" ) assert ( - scan.make_event("http://xn--12c1bik6bbd8ab6hd1b5jc6jta.com:80", dummy=True).data + scan.make_event("http://xn--12c1bik6bbd8ab6hd1b5jc6jta.com:80", dummy=True).url == "http://xn--12c1bik6bbd8ab6hd1b5jc6jta.com/" ) assert ( - scan.make_event("http://xn--12c1bik6bbd8ab6hd1b5jc6jta.com:80/ทดสอบ", dummy=True).data + scan.make_event("http://xn--12c1bik6bbd8ab6hd1b5jc6jta.com:80/ทดสอบ", dummy=True).url == "http://xn--12c1bik6bbd8ab6hd1b5jc6jta.com/ทดสอบ" ) @@ -450,10 +534,10 @@ async def test_events(events, helpers): assert scan.make_event("ทดสอบ@เราเที่ยวด้วยกัน.com", dummy=True).data == "ทดสอบ@xn--12c1bik6bbd8ab6hd1b5jc6jta.com" assert scan.make_event("เราเที่ยวด้วยกัน.com:80", dummy=True).data == "xn--12c1bik6bbd8ab6hd1b5jc6jta.com:80" assert ( - scan.make_event("http://เราเที่ยวด้วยกัน.com:80", dummy=True).data == "http://xn--12c1bik6bbd8ab6hd1b5jc6jta.com/" + scan.make_event("http://เราเที่ยวด้วยกัน.com:80", dummy=True).url == "http://xn--12c1bik6bbd8ab6hd1b5jc6jta.com/" ) assert ( - scan.make_event("http://เราเที่ยวด้วยกัน.com:80/ทดสอบ", dummy=True).data + scan.make_event("http://เราเที่ยวด้วยกัน.com:80/ทดสอบ", dummy=True).url == "http://xn--12c1bik6bbd8ab6hd1b5jc6jta.com/ทดสอบ" ) @@ -501,7 +585,7 @@ async def test_events(events, helpers): assert db_event.parent_chain[0] == str(db_event.uuid) assert db_event.parent.uuid == scan.root_event.uuid assert db_event.parent_uuid == scan.root_event.uuid - timestamp = db_event.timestamp.isoformat() + timestamp = db_event.timestamp.timestamp() json_event = db_event.json() assert isinstance(json_event["uuid"], str) assert json_event["uuid"] == str(db_event.uuid) @@ -522,7 +606,7 @@ async def test_events(events, helpers): assert reconstituted_event.uuid == db_event.uuid assert reconstituted_event.parent_uuid == scan.root_event.uuid assert reconstituted_event.scope_distance == 1 - assert reconstituted_event.timestamp.isoformat() == timestamp + assert reconstituted_event.timestamp.timestamp() == timestamp assert reconstituted_event.data == "evilcorp.com:80" assert reconstituted_event.type == "OPEN_TCP_PORT" assert reconstituted_event.host == "evilcorp.com" @@ -536,21 +620,6 @@ async def test_events(events, helpers): assert hostless_event_json["data"] == "asdf" assert "host" not in hostless_event_json - # SIEM-friendly serialize/deserialize - json_event_siemfriendly = db_event.json(siem_friendly=True) - assert json_event_siemfriendly["scope_distance"] == 1 - assert json_event_siemfriendly["data"] == {"OPEN_TCP_PORT": "evilcorp.com:80"} - assert json_event_siemfriendly["type"] == "OPEN_TCP_PORT" - assert json_event_siemfriendly["host"] == "evilcorp.com" - assert json_event_siemfriendly["timestamp"] == timestamp - reconstituted_event2 = event_from_json(json_event_siemfriendly, siem_friendly=True) - assert reconstituted_event2.scope_distance == 1 - assert reconstituted_event2.timestamp.isoformat() == timestamp - assert reconstituted_event2.data == "evilcorp.com:80" - assert reconstituted_event2.type == "OPEN_TCP_PORT" - assert reconstituted_event2.host == "evilcorp.com" - assert "127.0.0.1" in reconstituted_event2.resolved_hosts - http_response = scan.make_event(httpx_response, "HTTP_RESPONSE", parent=scan.root_event) assert http_response.parent_id == scan.root_event.id assert http_response.data["input"] == "http://example.com:80" @@ -559,9 +628,13 @@ async def test_events(events, helpers): == 'HTTP/1.1 200 OK\r\nConnection: close\r\nAge: 526111\r\nCache-Control: max-age=604800\r\nContent-Type: text/html; charset=UTF-8\r\nDate: Mon, 14 Nov 2022 17:14:27 GMT\r\nEtag: "3147526947+ident+gzip"\r\nExpires: Mon, 21 Nov 2022 17:14:27 GMT\r\nLast-Modified: Thu, 17 Oct 2019 07:18:26 GMT\r\nServer: ECS (agb/A445)\r\nVary: Accept-Encoding\r\nX-Cache: HIT\r\n\r\n\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n' ) json_event = http_response.json(mode="graph") + assert "data" in json_event + assert "data_json" not in json_event assert isinstance(json_event["data"], str) json_event = http_response.json() - assert isinstance(json_event["data"], dict) + assert "data" not in json_event + assert "data_json" in json_event + assert isinstance(json_event["data_json"], dict) assert json_event["type"] == "HTTP_RESPONSE" assert json_event["host"] == "example.com" assert json_event["parent"] == scan.root_event.id @@ -635,6 +708,7 @@ async def test_event_discovery_context(): from bbot.modules.base import BaseModule scan = Scanner("evilcorp.com") + await scan._prep() await scan.helpers.dns._mock_dns( { "evilcorp.com": {"A": ["1.2.3.4"]}, @@ -644,7 +718,6 @@ async def test_event_discovery_context(): "four.evilcorp.com": {"A": ["1.2.3.4"]}, } ) - await scan._prep() dummy_module_1 = scan._make_dummy_module("module_1") dummy_module_2 = scan._make_dummy_module("module_2") @@ -794,6 +867,7 @@ async def handle_event(self, event): # test to make sure this doesn't come back # https://github.com/blacklanternsecurity/bbot/issues/1498 scan = Scanner("http://blacklanternsecurity.com", config={"dns": {"minimal": False}}) + await scan._prep() await scan.helpers.dns._mock_dns( {"blacklanternsecurity.com": {"TXT": ["blsops.com"], "A": ["127.0.0.1"]}, "blsops.com": {"A": ["127.0.0.1"]}} ) @@ -812,6 +886,7 @@ async def test_event_web_spider_distance(bbot_scanner): # URL_UNVERIFIED events should not increment web spider distance scan = bbot_scanner(config={"web": {"spider_distance": 1}}) + await scan._prep() url_event_1 = scan.make_event("http://www.evilcorp.com/test1", "URL_UNVERIFIED", parent=scan.root_event) assert url_event_1.web_spider_distance == 0 url_event_2 = scan.make_event("http://www.evilcorp.com/test2", "URL_UNVERIFIED", parent=url_event_1) @@ -825,6 +900,7 @@ async def test_event_web_spider_distance(bbot_scanner): # URL events should increment web spider distance scan = bbot_scanner(config={"web": {"spider_distance": 1}}) + await scan._prep() url_event_1 = scan.make_event("http://www.evilcorp.com/test1", "URL", parent=scan.root_event, tags="status-200") assert url_event_1.web_spider_distance == 0 url_event_2 = scan.make_event("http://www.evilcorp.com/test2", "URL", parent=url_event_1, tags="status-200") @@ -897,43 +973,10 @@ async def test_event_web_spider_distance(bbot_scanner): assert "spider-max" not in url_event_5.tags -def test_event_confidence(): - scan = Scanner() - # default 100 - event1 = scan.make_event("evilcorp.com", "DNS_NAME", dummy=True) - assert event1.confidence == 100 - assert event1.cumulative_confidence == 100 - # custom confidence - event2 = scan.make_event("evilcorp.com", "DNS_NAME", confidence=90, dummy=True) - assert event2.confidence == 90 - assert event2.cumulative_confidence == 90 - # max 100 - event3 = scan.make_event("evilcorp.com", "DNS_NAME", confidence=999, dummy=True) - assert event3.confidence == 100 - assert event3.cumulative_confidence == 100 - # min 1 - event4 = scan.make_event("evilcorp.com", "DNS_NAME", confidence=0, dummy=True) - assert event4.confidence == 1 - assert event4.cumulative_confidence == 1 - # first event in chain - event5 = scan.make_event("evilcorp.com", "DNS_NAME", confidence=90, parent=scan.root_event) - assert event5.confidence == 90 - assert event5.cumulative_confidence == 90 - # compounding confidence - event6 = scan.make_event("evilcorp.com", "DNS_NAME", confidence=50, parent=event5) - assert event6.confidence == 50 - assert event6.cumulative_confidence == 45 - event7 = scan.make_event("evilcorp.com", "DNS_NAME", confidence=50, parent=event6) - assert event7.confidence == 50 - assert event7.cumulative_confidence == 22 - # 100 confidence resets - event8 = scan.make_event("evilcorp.com", "DNS_NAME", confidence=100, parent=event7) - assert event8.confidence == 100 - assert event8.cumulative_confidence == 100 - - -def test_event_closest_host(): +@pytest.mark.asyncio +async def test_event_closest_host(): scan = Scanner() + await scan._prep() # first event has a host event1 = scan.make_event("evilcorp.com", "DNS_NAME", parent=scan.root_event) assert event1.host == "evilcorp.com" @@ -953,13 +996,21 @@ def test_event_closest_host(): event3 = scan.make_event({"path": "/tmp/asdf.txt"}, "FILESYSTEM", parent=event2) assert not event3.host # finding automatically uses the host from the second event - finding = scan.make_event({"description": "test"}, "FINDING", parent=event3) + finding = scan.make_event( + {"description": "test", "severity": "LOW", "confidence": "MEDIUM", "name": "Test Finding"}, + "FINDING", + parent=event3, + ) assert finding.data["host"] == "www.evilcorp.com" assert finding.data["url"] == "http://www.evilcorp.com/asdf" assert finding.data["path"] == "/tmp/asdf.txt" assert finding.host == "www.evilcorp.com" # same with vuln - vuln = scan.make_event({"description": "test", "severity": "HIGH"}, "VULNERABILITY", parent=event3) + vuln = scan.make_event( + {"description": "test", "severity": "HIGH", "confidence": "HIGH", "name": "Test Finding"}, + "FINDING", + parent=event3, + ) assert vuln.data["host"] == "www.evilcorp.com" assert vuln.data["url"] == "http://www.evilcorp.com/asdf" assert vuln.data["path"] == "/tmp/asdf.txt" @@ -969,24 +1020,69 @@ def test_event_closest_host(): event3 = scan.make_event("wat", "ASDF", parent=scan.root_event) assert not event3.host with pytest.raises(ValueError): - finding = scan.make_event({"description": "test"}, "FINDING", parent=event3) - finding = scan.make_event({"path": "/tmp/asdf.txt", "description": "test"}, "FINDING", parent=event3) + finding = scan.make_event( + {"description": "test", "severity": "LOW", "confidence": "MEDIUM", "name": "Test Finding"}, + "FINDING", + parent=event3, + ) + finding = scan.make_event( + { + "path": "/tmp/asdf.txt", + "description": "test", + "severity": "LOW", + "confidence": "MEDIUM", + "name": "Test Finding", + }, + "FINDING", + parent=event3, + ) assert finding is not None - finding = scan.make_event({"host": "evilcorp.com", "description": "test"}, "FINDING", parent=event3) + finding = scan.make_event( + { + "host": "evilcorp.com", + "description": "test", + "severity": "LOW", + "confidence": "MEDIUM", + "name": "Test Finding", + }, + "FINDING", + parent=event3, + ) assert finding is not None with pytest.raises(ValueError): - vuln = scan.make_event({"description": "test", "severity": "HIGH"}, "VULNERABILITY", parent=event3) + vuln = scan.make_event( + {"description": "test", "severity": "HIGH", "confidence": "CONFIRMED", "name": "Test Finding"}, + "FINDING", + parent=event3, + ) vuln = scan.make_event( - {"path": "/tmp/asdf.txt", "description": "test", "severity": "HIGH"}, "VULNERABILITY", parent=event3 + { + "path": "/tmp/asdf.txt", + "description": "test", + "severity": "HIGH", + "confidence": "CONFIRMED", + "name": "Test Finding", + }, + "FINDING", + parent=event3, ) assert vuln is not None vuln = scan.make_event( - {"host": "evilcorp.com", "description": "test", "severity": "HIGH"}, "VULNERABILITY", parent=event3 + { + "host": "evilcorp.com", + "description": "test", + "severity": "HIGH", + "confidence": "CONFIRMED", + "name": "Test Finding", + }, + "FINDING", + parent=event3, ) assert vuln is not None -def test_event_magic(): +@pytest.mark.asyncio +async def test_event_magic(): from bbot.core.helpers.libmagic import get_magic_info, get_compression import base64 @@ -1007,6 +1103,7 @@ def test_event_magic(): # test filesystem event - file scan = Scanner() + await scan._prep() event = scan.make_event({"path": zip_file}, "FILESYSTEM", parent=scan.root_event) assert event.data == { "path": "/tmp/.bbottestzipasdkfjalsdf.zip", @@ -1020,6 +1117,7 @@ def test_event_magic(): # test filesystem event - folder scan = Scanner() + await scan._prep() event = scan.make_event({"path": "/tmp"}, "FILESYSTEM", parent=scan.root_event) assert event.data == {"path": "/tmp"} assert event.tags == {"folder"} @@ -1030,6 +1128,7 @@ def test_event_magic(): @pytest.mark.asyncio async def test_mobile_app(): scan = Scanner() + await scan._prep() with pytest.raises(ValidationError): scan.make_event("com.evilcorp.app", "MOBILE_APP", parent=scan.root_event) with pytest.raises(ValidationError): @@ -1058,6 +1157,7 @@ async def test_mobile_app(): @pytest.mark.asyncio async def test_filesystem(): scan = Scanner("FILESYSTEM:/tmp/asdfasdgasdfasdfddsdf") + await scan._prep() events = [e async for e in scan.async_start()] assert len(events) == 3 filesystem_events = [e for e in events if e.type == "FILESYSTEM"] @@ -1066,26 +1166,42 @@ async def test_filesystem(): assert filesystem_events[0].data == {"path": "/tmp/asdfasdgasdfasdfddsdf"} -def test_event_hashing(): +@pytest.mark.asyncio +async def test_event_hashing(): scan = Scanner("example.com") + await scan._prep() url_event = scan.make_event("https://api.example.com/", "URL_UNVERIFIED", parent=scan.root_event) host_event_1 = scan.make_event("www.example.com", "DNS_NAME", parent=url_event) host_event_2 = scan.make_event("test.example.com", "DNS_NAME", parent=url_event) - finding_data = {"description": "Custom Yara Rule [find_string] Matched via identifier [str1]"} + finding_data = { + "description": "Custom Yara Rule [find_string] Matched via identifier [str1]", + "severity": "MEDIUM", + "confidence": "HIGH", + "name": "Finding", + } finding1 = scan.make_event(finding_data, "FINDING", parent=host_event_1) finding2 = scan.make_event(finding_data, "FINDING", parent=host_event_2) finding3 = scan.make_event(finding_data, "FINDING", parent=host_event_2) assert finding1.data == { "description": "Custom Yara Rule [find_string] Matched via identifier [str1]", + "name": "Finding", + "severity": "MEDIUM", + "confidence": "HIGH", "host": "www.example.com", } assert finding2.data == { "description": "Custom Yara Rule [find_string] Matched via identifier [str1]", + "name": "Finding", + "severity": "MEDIUM", + "confidence": "HIGH", "host": "test.example.com", } assert finding3.data == { "description": "Custom Yara Rule [find_string] Matched via identifier [str1]", + "name": "Finding", + "severity": "MEDIUM", + "confidence": "HIGH", "host": "test.example.com", } assert finding1.id != finding2.id @@ -1096,3 +1212,62 @@ def test_event_hashing(): assert finding2.data_hash == finding3.data_hash assert hash(finding1) != hash(finding2) assert hash(finding2) == hash(finding3) + + +@pytest.mark.asyncio +async def test_host_metadata(): + scan = Scanner("example.com") + await scan._prep() + + # host_metadata should be lazy-initialized as empty dict + dns_event = scan.make_event("example.com", "DNS_NAME", parent=scan.root_event) + assert dns_event.host_metadata == {} + + # set host_metadata + dns_event.host_metadata = { + "example.com": { + "cloud_providers": { + "cloudflare": {"types": ["waf"], "match": "domain"}, + } + }, + "104.18.26.217": { + "cloud_providers": { + "cloudflare": {"types": ["waf"], "match": "ip"}, + "amazon": {"types": ["cloud"], "match": "ip"}, + } + }, + } + + # verify access + assert "cloudflare" in dns_event.host_metadata["example.com"]["cloud_providers"] + assert dns_event.host_metadata["104.18.26.217"]["cloud_providers"]["amazon"]["match"] == "ip" + + # verify JSON serialization + j = dns_event.json() + assert "host_metadata" in j + assert j["host_metadata"]["example.com"]["cloud_providers"]["cloudflare"]["types"] == ["waf"] + assert j["host_metadata"]["104.18.26.217"]["cloud_providers"]["amazon"]["match"] == "ip" + + # verify host_metadata is NOT serialized when empty + dns_event2 = scan.make_event("test.example.com", "DNS_NAME", parent=scan.root_event) + j2 = dns_event2.json() + assert "host_metadata" not in j2 + + # URL events also have host_metadata + url_event = scan.make_event("https://example.com/", "URL_UNVERIFIED", parent=scan.root_event) + assert url_event.host_metadata == {} + url_event.host_metadata["example.com"] = {"cloud_providers": {"google": {"types": ["cloud"], "match": "domain"}}} + j3 = url_event.json() + assert j3["host_metadata"]["example.com"]["cloud_providers"]["google"]["types"] == ["cloud"] + + # URL event data dict should contain url, and optionally http_title/status_code + assert url_event.data["url"] == "https://example.com/" + url_event.http_title = "Example Domain" + url_event.data["status_code"] = 200 + assert url_event.data["http_title"] == "Example Domain" + assert url_event.http_status == 200 + j4 = url_event.json() + assert j4["data_json"]["http_title"] == "Example Domain" + assert j4["data_json"]["status_code"] == 200 + + await scan._cleanup() diff --git a/bbot/test/test_step_1/test_files.py b/bbot/test/test_step_1/test_files.py index feb6b928c3..300742990a 100644 --- a/bbot/test/test_step_1/test_files.py +++ b/bbot/test/test_step_1/test_files.py @@ -6,6 +6,7 @@ @pytest.mark.asyncio async def test_files(bbot_scanner): scan1 = bbot_scanner() + await scan1._prep() # tempfile tempfile = scan1.helpers.tempfile(("line1", "line2"), pipe=False) diff --git a/bbot/test/test_step_1/test_helpers.py b/bbot/test/test_step_1/test_helpers.py index fa54bcbe7f..68bb524341 100644 --- a/bbot/test/test_step_1/test_helpers.py +++ b/bbot/test/test_step_1/test_helpers.py @@ -590,7 +590,7 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): await scan._cleanup() scan1 = bbot_scanner(modules="ipneighbor") - await scan1.load_modules() + await scan1._prep() assert int(helpers.get_size(scan1.modules["ipneighbor"])) > 0 await scan1._cleanup() @@ -661,6 +661,7 @@ async def test_word_cloud(helpers, bbot_scanner): # saving and loading scan1 = bbot_scanner("127.0.0.1") + await scan1._prep() word_cloud = scan1.helpers.word_cloud word_cloud.add_word("lantern") word_cloud.add_word("black") @@ -975,3 +976,161 @@ async def test_rm_temp_dir_at_exit(helpers): # temp dir should be removed assert not temp_dir.exists() + + +def test_simhash_similarity(helpers): + """Test SimHash helper with increasingly different HTML pages.""" + + # Base HTML page + base_html = """ + + + + Example Page + + + +

Welcome to Example Corp

+
+

This is the main content of our website.

+

We provide excellent services to our customers.

+
    +
  • Service A
  • +
  • Service B
  • +
  • Service C
  • +
+
+
Copyright 2024 Example Corp
+ + + """ + + # Slightly different - changed one word + slightly_different = """ + + + + Example Page + + + +

Welcome to Example Corp

+
+

This is the main content of our website.

+

We provide amazing services to our customers.

+
    +
  • Service A
  • +
  • Service B
  • +
  • Service C
  • +
+
+
Copyright 2024 Example Corp
+ + + """ + + # Moderately different - changed content section + moderately_different = """ + + + + Example Page + + + +

Welcome to Example Corp

+
+

This page contains different information.

+

Our products are innovative and cutting-edge.

+
    +
  • Product X
  • +
  • Product Y
  • +
  • Product Z
  • +
+
+
Copyright 2024 Example Corp
+ + + """ + + # Very different - completely different content + very_different = """ + + + + News Portal + + + +

Latest News

+
+
+

Breaking News Today

+

Important events are happening around the world.

+
+
+

Sports Update

+

Local team wins championship game.

+
+
+
News Corp 2024
+ + + """ + + # Completely different - different structure and content + completely_different = """ + + + + 300 + 5 + + + Result A + Result B + + + """ + + # Test SimHash similarity + simhash = helpers.simhash + + # Calculate hashes + base_hash = simhash.hash(base_html) + slightly_hash = simhash.hash(slightly_different) + moderately_hash = simhash.hash(moderately_different) + very_hash = simhash.hash(very_different) + completely_hash = simhash.hash(completely_different) + + # Calculate similarities + identical_similarity = simhash.similarity(base_hash, base_hash) + slight_similarity = simhash.similarity(base_hash, slightly_hash) + moderate_similarity = simhash.similarity(base_hash, moderately_hash) + very_similarity = simhash.similarity(base_hash, very_hash) + complete_similarity = simhash.similarity(base_hash, completely_hash) + + print(f"Identical: {identical_similarity:.3f}") + print(f"Slightly different: {slight_similarity:.3f}") + print(f"Moderately different: {moderate_similarity:.3f}") + print(f"Very different: {very_similarity:.3f}") + print(f"Completely different: {complete_similarity:.3f}") + + # Verify expected similarity ordering + assert identical_similarity == 1.0, "Identical content should have similarity of 1.0" + assert slight_similarity > moderate_similarity, ( + "Slightly different should be more similar than moderately different" + ) + assert moderate_similarity > very_similarity, "Moderately different should be more similar than very different" + assert very_similarity > complete_similarity, "Very different should be more similar than completely different" + + # Verify reasonable similarity ranges based on actual SimHash behavior + # With 64-bit hashes and 3-character shingles, we get good differentiation + assert slight_similarity > 0.90, "Slightly different content should be highly similar (>0.90)" + assert moderate_similarity > 0.70, "Moderately different content should be quite similar (>0.70)" + assert very_similarity > 0.50, "Very different content should have medium similarity (>0.50)" + assert complete_similarity > 0.30, "Completely different content should have low similarity (>0.30)" + assert complete_similarity < 0.50, "Completely different content should be clearly different (<0.50)" + + # Most importantly, verify the ordering is correct + assert identical_similarity > slight_similarity > moderate_similarity > very_similarity > complete_similarity diff --git a/bbot/test/test_step_1/test_manager_deduplication.py b/bbot/test/test_step_1/test_manager_deduplication.py index 65fbaeb172..9151ad7da8 100644 --- a/bbot/test/test_step_1/test_manager_deduplication.py +++ b/bbot/test/test_step_1/test_manager_deduplication.py @@ -48,18 +48,29 @@ class PerDomainOnly(DefaultModule): async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs): scan = bbot_scanner(*args, config=_config, **kwargs) + await scan._prep() default_module = DefaultModule(scan) everything_module = EverythingModule(scan) no_suppress_dupes = NoSuppressDupes(scan) accept_dupes = AcceptDupes(scan) per_hostport_only = PerHostOnly(scan) per_domain_only = PerDomainOnly(scan) + + # Add modules to scan scan.modules["default_module"] = default_module scan.modules["everything_module"] = everything_module scan.modules["no_suppress_dupes"] = no_suppress_dupes scan.modules["accept_dupes"] = accept_dupes scan.modules["per_hostport_only"] = per_hostport_only scan.modules["per_domain_only"] = per_domain_only + + # Setup each module manually since they were added after _prep() + modules_to_setup = [default_module, everything_module, no_suppress_dupes, accept_dupes, per_hostport_only, per_domain_only] + for module in modules_to_setup: + setup_result = await module.setup() + if setup_result is not True: + raise Exception(f"Module {module.name} setup failed: {setup_result}") + if _dns_mock: await scan.helpers.dns._mock_dns(_dns_mock) if scan_callback is not None: @@ -101,7 +112,7 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "no_suppress_dupes.test.notreal" and str(e.module) == "no_suppress_dupes" and e.parent.data == "test.notreal"]) assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "per_domain_only.test.notreal" and str(e.module) == "per_domain_only"]) assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "per_hostport_only.test.notreal" and str(e.module) == "per_hostport_only"]) - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "TARGET" and "SCAN:" in e.parent.data["id"]]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "SEED" and "SCAN:" in e.parent.data["id"]]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "accept_dupes.test.notreal:88" and str(e.module) == "everything_module" and e.parent.data == "accept_dupes.test.notreal"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "default_module.test.notreal:88" and str(e.module) == "everything_module" and e.parent.data == "default_module.test.notreal"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "per_domain_only.test.notreal:88" and str(e.module) == "everything_module" and e.parent.data == "per_domain_only.test.notreal"]) @@ -115,7 +126,7 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) assert 1 == len([e for e in default_events if e.type == "DNS_NAME" and e.data == "no_suppress_dupes.test.notreal" and str(e.module) == "no_suppress_dupes"]) assert 1 == len([e for e in default_events if e.type == "DNS_NAME" and e.data == "per_domain_only.test.notreal" and str(e.module) == "per_domain_only"]) assert 1 == len([e for e in default_events if e.type == "DNS_NAME" and e.data == "per_hostport_only.test.notreal" and str(e.module) == "per_hostport_only"]) - assert 1 == len([e for e in default_events if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "TARGET" and "SCAN:" in e.parent.data["id"]]) + assert 1 == len([e for e in default_events if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "SEED" and "SCAN:" in e.parent.data["id"]]) assert len(all_events) == 27 assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "accept_dupes.test.notreal" and str(e.module) == "accept_dupes"]) @@ -127,7 +138,7 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "no_suppress_dupes.test.notreal" and str(e.module) == "no_suppress_dupes" and e.parent.data == "test.notreal"]) assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "per_domain_only.test.notreal" and str(e.module) == "per_domain_only"]) assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "per_hostport_only.test.notreal" and str(e.module) == "per_hostport_only"]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "TARGET" and "SCAN:" in e.parent.data["id"]]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "SEED" and "SCAN:" in e.parent.data["id"]]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.3" and str(e.module) == "A" and e.parent.data == "test.notreal"]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.3" and str(e.module) == "A" and e.parent.data == "default_module.test.notreal"]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.5" and str(e.module) == "A" and e.parent.data == "no_suppress_dupes.test.notreal"]) @@ -147,7 +158,7 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) assert 1 == len([e for e in no_suppress_dupes if e.type == "DNS_NAME" and e.data == "no_suppress_dupes.test.notreal" and str(e.module) == "no_suppress_dupes"]) assert 1 == len([e for e in no_suppress_dupes if e.type == "DNS_NAME" and e.data == "per_domain_only.test.notreal" and str(e.module) == "per_domain_only"]) assert 1 == len([e for e in no_suppress_dupes if e.type == "DNS_NAME" and e.data == "per_hostport_only.test.notreal" and str(e.module) == "per_hostport_only"]) - assert 1 == len([e for e in no_suppress_dupes if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "TARGET" and "SCAN:" in e.parent.data["id"]]) + assert 1 == len([e for e in no_suppress_dupes if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "SEED" and "SCAN:" in e.parent.data["id"]]) assert len(accept_dupes) == 10 assert 1 == len([e for e in accept_dupes if e.type == "DNS_NAME" and e.data == "accept_dupes.test.notreal" and str(e.module) == "accept_dupes"]) @@ -159,7 +170,7 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) assert 1 == len([e for e in accept_dupes if e.type == "DNS_NAME" and e.data == "no_suppress_dupes.test.notreal" and str(e.module) == "no_suppress_dupes" and e.parent.data == "test.notreal"]) assert 1 == len([e for e in accept_dupes if e.type == "DNS_NAME" and e.data == "per_domain_only.test.notreal" and str(e.module) == "per_domain_only"]) assert 1 == len([e for e in accept_dupes if e.type == "DNS_NAME" and e.data == "per_hostport_only.test.notreal" and str(e.module) == "per_hostport_only"]) - assert 1 == len([e for e in accept_dupes if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "TARGET" and "SCAN:" in e.parent.data["id"]]) + assert 1 == len([e for e in accept_dupes if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "SEED" and "SCAN:" in e.parent.data["id"]]) assert len(per_hostport_only) == 6 assert 1 == len([e for e in per_hostport_only if e.type == "DNS_NAME" and e.data == "accept_dupes.test.notreal" and str(e.module) == "accept_dupes"]) @@ -167,7 +178,7 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) assert 1 == len([e for e in per_hostport_only if e.type == "DNS_NAME" and e.data == "no_suppress_dupes.test.notreal" and str(e.module) == "no_suppress_dupes"]) assert 1 == len([e for e in per_hostport_only if e.type == "DNS_NAME" and e.data == "per_domain_only.test.notreal" and str(e.module) == "per_domain_only"]) assert 1 == len([e for e in per_hostport_only if e.type == "DNS_NAME" and e.data == "per_hostport_only.test.notreal" and str(e.module) == "per_hostport_only"]) - assert 1 == len([e for e in per_hostport_only if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "TARGET" and "SCAN:" in e.parent.data["id"]]) + assert 1 == len([e for e in per_hostport_only if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "SEED" and "SCAN:" in e.parent.data["id"]]) assert len(per_domain_only) == 1 - assert 1 == len([e for e in per_domain_only if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "TARGET" and "SCAN:" in e.parent.data["id"]]) + assert 1 == len([e for e in per_domain_only if e.type == "DNS_NAME" and e.data == "test.notreal" and str(e.module) == "SEED" and "SCAN:" in e.parent.data["id"]]) diff --git a/bbot/test/test_step_1/test_manager_scope_accuracy.py b/bbot/test/test_step_1/test_manager_scope_accuracy.py index 41e55ea342..161fad5958 100644 --- a/bbot/test/test_step_1/test_manager_scope_accuracy.py +++ b/bbot/test/test_step_1/test_manager_scope_accuracy.py @@ -42,7 +42,7 @@ def bbot_other_httpservers(): @pytest.mark.asyncio -async def test_manager_scope_accuracy(bbot_scanner, bbot_httpserver, bbot_other_httpservers, bbot_httpserver_ssl): +async def test_manager_scope_accuracy_correct(bbot_scanner, bbot_httpserver, bbot_other_httpservers, bbot_httpserver_ssl): """ This test ensures that BBOT correctly handles different scope distance settings. It performs these tests for normal modules, output modules, and their graph variants, @@ -103,18 +103,27 @@ async def handle_batch(self, *events): async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs): scan = bbot_scanner(*args, config=_config, **kwargs) + await scan._prep() dummy_module = DummyModule(scan) dummy_module_nodupes = DummyModuleNoDupes(scan) dummy_graph_output_module = DummyGraphOutputModule(scan) dummy_graph_batch_output_module = DummyGraphBatchOutputModule(scan) + await dummy_module.setup() + await dummy_module_nodupes.setup() + await dummy_graph_output_module.setup() + await dummy_graph_batch_output_module.setup() + scan.modules["dummy_module"] = dummy_module scan.modules["dummy_module_nodupes"] = dummy_module_nodupes scan.modules["dummy_graph_output_module"] = dummy_graph_output_module scan.modules["dummy_graph_batch_output_module"] = dummy_graph_batch_output_module + await scan.helpers.dns._mock_dns(_dns_mock) if scan_callback is not None: scan_callback(scan) output_events = [e async for e in scan.async_start()] + # let modules initialize + await asyncio.sleep(0.5) return ( output_events, dummy_module.events, @@ -268,7 +277,7 @@ async def filter_event(self, event): async def handle_event(self, event): await self.emit_event( - {"host": str(event.host), "description": "yep", "severity": "CRITICAL"}, "VULNERABILITY", parent=event + {"host": str(event.host), "description": "yep", "severity": "CRITICAL", "confidence": "CONFIRMED", "name": "Test Finding"}, "FINDING", parent=event ) def custom_setup(scan): @@ -288,21 +297,21 @@ def custom_setup(scan): assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notrealzies"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77"]) - assert 1 == len([e for e in events if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) + assert 1 == len([e for e in events if e.type == "FINDING" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) assert len(all_events) == 8 assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) assert 2 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is True and e.scope_distance == 2]) assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is True and e.scope_distance == 3]) - assert 1 == len([e for e in all_events if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) + assert 1 == len([e for e in all_events if e.type == "FINDING" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) assert len(all_events_nodups) == 6 assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is True and e.scope_distance == 2]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is True and e.scope_distance == 3]) - assert 1 == len([e for e in all_events_nodups if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) + assert 1 == len([e for e in all_events_nodups if e.type == "FINDING" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 7 @@ -310,7 +319,7 @@ def custom_setup(scan): assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is True and e.scope_distance == 2]) assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is True and e.scope_distance == 3]) - assert 1 == len([e for e in _graph_output_events if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) + assert 1 == len([e for e in _graph_output_events if e.type == "FINDING" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) # httpx/speculate IP_RANGE --> IP_ADDRESS --> OPEN_TCP_PORT --> URL, search distance = 0 events, all_events, all_events_nodups, graph_output_events, graph_output_batch_events = await do_scan( @@ -333,10 +342,10 @@ def custom_setup(scan): assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/"]) assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) @@ -346,10 +355,10 @@ def custom_setup(scan): assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) @@ -359,10 +368,10 @@ def custom_setup(scan): assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) @@ -373,10 +382,10 @@ def custom_setup(scan): assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/"]) assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) @@ -405,16 +414,16 @@ def custom_setup(scan): assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/"]) assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.77:8888"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/"]) assert len(all_events) == 18 assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) @@ -422,16 +431,16 @@ def custom_setup(scan): assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal is True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) assert len(all_events_nodups) == 16 assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) @@ -439,16 +448,16 @@ def custom_setup(scan): assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal is True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 8 @@ -457,16 +466,16 @@ def custom_setup(scan): assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and "spider-danger" in e.tags]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/" and "spider-danger" in e.tags]) assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.88:8888/"]) # httpx/speculate IP_RANGE --> IP_ADDRESS --> OPEN_TCP_PORT --> URL, search distance = 1 events, all_events, all_events_nodups, graph_output_events, graph_output_batch_events = await do_scan( @@ -488,16 +497,16 @@ def custom_setup(scan): assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/"]) assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/"]) assert len(all_events) == 22 assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) @@ -505,20 +514,20 @@ def custom_setup(scan): assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal is True and e.scope_distance == 2]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.88:8888" and e.internal is True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.url == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.99:8888/" and e.internal is True and e.scope_distance == 3]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.99:8888/" and e.internal is True and e.scope_distance == 3]) assert len(all_events_nodups) == 20 assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) @@ -526,20 +535,20 @@ def custom_setup(scan): assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal is True and e.scope_distance == 2]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.88:8888" and e.internal is True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.url == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.99:8888/" and e.internal is True and e.scope_distance == 3]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.99:8888/" and e.internal is True and e.scope_distance == 3]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 8 @@ -548,21 +557,21 @@ def custom_setup(scan): assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.77:8888/"]) assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.url == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.88:8888/"]) # 2 events from a single HTTP_RESPONSE events, all_events, all_events_nodups, graph_output_events, graph_output_batch_events = await do_scan( - "127.0.0.111/31", - whitelist=["127.0.0.111/31", "127.0.0.222", "127.0.0.33"], + "127.0.0.111/31", "127.0.0.222", "127.0.0.33", + seeds=["127.0.0.111/31"], modules=["httpx"], output_modules=["python"], _config={ @@ -581,24 +590,24 @@ def custom_setup(scan): assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.111:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.222:8889/"]) assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.222" and e.internal is False and e.scope_distance == 0]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.33:8889/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.33:8889/"]) assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.33" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8888"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889"]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.222:8889"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.33:8889"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.44:8888/"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.44"]) - assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.55:8888/"]) + assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.55:8888/"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.55"]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.44:8888"]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.55:8888"]) @@ -609,12 +618,12 @@ def custom_setup(scan): assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888" and e.internal is True and e.scope_distance == 0]) assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.url == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.222" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.33" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8888" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889" and e.internal is True and e.scope_distance == 0]) @@ -622,13 +631,13 @@ def custom_setup(scan): assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.url == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.url == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.44:8888/" and e.internal is True and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.44" and e.internal is True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.55:8888/" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.55:8888/" and e.internal is True and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.55" and e.internal is True and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.44:8888" and e.internal is True and e.scope_distance == 1]) assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.55:8888" and e.internal is True and e.scope_distance == 1]) @@ -639,24 +648,24 @@ def custom_setup(scan): assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.url == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.222" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.33" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8888" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888" and e.internal is True and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889" and e.internal is True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.url == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.url == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.44:8888/" and e.internal is True and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.44" and e.internal is True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.55:8888/" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.55:8888/" and e.internal is True and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.55" and e.internal is True and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.44:8888" and e.internal is True and e.scope_distance == 1]) assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.55:8888" and e.internal is True and e.scope_distance == 1]) @@ -668,24 +677,24 @@ def custom_setup(scan): assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888"]) assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.url == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.111:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.222:8889/"]) assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.222"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.33:8889/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.33:8889/"]) assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.33"]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8888"]) assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889"]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888"]) assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889"]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.url == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.222:8889"]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.url == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.33:8889"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.44:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.44"]) - assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.55:8888/"]) + assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.55:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.55"]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.44:8888"]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.55:8888"]) @@ -748,9 +757,9 @@ def custom_setup(scan): # sslcert with out-of-scope chain events, all_events, all_events_nodups, graph_output_events, graph_output_batch_events = await do_scan( - "127.0.0.0/31", + "127.0.1.0", + seeds=["127.0.0.0/31"], modules=["sslcert"], - whitelist=["127.0.1.0"], _config={"scope": {"search_distance": 1, "report_distance": 0}, "speculate": True, "modules": {"speculate": {"ports": "9999"}}}, _dns_mock={"www.bbottest.notreal": {"A": ["127.0.0.1"]}, "test.notreal": {"A": ["127.0.1.0"]}}, ) @@ -804,12 +813,13 @@ async def test_manager_blacklist(bbot_scanner, bbot_httpserver, caplog): # dns search distance = 1, report distance = 0 scan = bbot_scanner( - "http://127.0.0.1:8888", + "127.0.0.0/29", "test.notreal", + seeds=["http://127.0.0.1:8888"], modules=["httpx"], config={"excavate": True, "dns": {"minimal": False, "search_distance": 1}, "scope": {"report_distance": 0}}, - whitelist=["127.0.0.0/29", "test.notreal"], blacklist=["127.0.0.64/29"], ) + await scan._prep() await scan.helpers.dns._mock_dns({ "www-prod.test.notreal": {"A": ["127.0.0.66"]}, "www-dev.test.notreal": {"A": ["127.0.0.22"]}, @@ -817,9 +827,9 @@ async def test_manager_blacklist(bbot_scanner, bbot_httpserver, caplog): events = [e async for e in scan.async_start()] - assert any(e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://www-dev.test.notreal:8888/") + assert any(e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://www-dev.test.notreal:8888/") # the hostname is in-scope, but its IP is blacklisted, therefore we shouldn't see it - assert not any(e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://www-prod.test.notreal:8888/") + assert not any(e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://www-prod.test.notreal:8888/") assert 'Not forwarding DNS_NAME("www-prod.test.notreal", module=excavate' in caplog.text and 'because it has a blacklisted DNS record' in caplog.text @@ -827,6 +837,7 @@ async def test_manager_blacklist(bbot_scanner, bbot_httpserver, caplog): @pytest.mark.asyncio async def test_manager_scope_tagging(bbot_scanner): scan = bbot_scanner("test.notreal") + await scan._prep() e1 = scan.make_event("www.test.notreal", parent=scan.root_event, tags=["affiliate"]) assert e1.scope_distance == 1 assert "distance-1" in e1.tags @@ -887,7 +898,7 @@ async def handle_event(self, event): # there are actually 2 URL events. They are both from the same URL, but one was extracted by the full URL regex, and the other by the src/href= regex. # however, they should be deduped by scan_ingress. - bad_url_events = [e for e in dummy_module.events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/asdf.js"] + bad_url_events = [e for e in dummy_module.events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/asdf.js"] assert len(bad_url_events) == 1 # they should both be internal assert all(e.internal is True for e in bad_url_events) diff --git a/bbot/test/test_step_1/test_modules_basic.py b/bbot/test/test_step_1/test_modules_basic.py index 830c8a3f7b..8054e3c95d 100644 --- a/bbot/test/test_step_1/test_modules_basic.py +++ b/bbot/test/test_step_1/test_modules_basic.py @@ -13,10 +13,9 @@ async def test_modules_basic_checks(events, httpx_mock): from bbot.scanner import Scanner scan = Scanner(config={"omit_event_types": ["URL_UNVERIFIED"]}) + await scan._prep() assert "URL_UNVERIFIED" in scan.omitted_event_types - await scan.load_modules() - # output module specific event filtering tests base_output_module_1 = BaseOutputModule(scan) base_output_module_1.watched_events = ["IP_ADDRESS", "URL_UNVERIFIED"] @@ -89,6 +88,36 @@ async def test_modules_basic_checks(events, httpx_mock): await egress_module.handle_event(url) assert url._omit is True + # omitted always_emit events should still be omitted + finding_data = { + "host": "evilcorp.com", + "description": "test", + "severity": "LOW", + "confidence": "LOW", + "name": "test", + } + finding = scan.make_event(finding_data, "FINDING", parent=scan.root_event) + assert finding.always_emit is True + finding._omit = True + result, reason = base_output_module_2._event_precheck(finding) + assert result is False + assert reason == "its type is omitted in the config" + + # always_emit should bypass the internal check + finding_data2 = { + "host": "evilcorp.com", + "description": "test2", + "severity": "LOW", + "confidence": "LOW", + "name": "test2", + } + finding2 = scan.make_event(finding_data2, "FINDING", parent=scan.root_event) + assert finding2.always_emit is True + finding2._internal = True + result, reason = base_output_module_2._event_precheck(finding2) + assert result is True + assert reason == "event is always emitted" + # common event filtering tests for module_class in (BaseModule, BaseOutputModule, BaseReportModule, BaseInternalModule): base_module = module_class(scan) @@ -150,11 +179,18 @@ async def test_modules_basic_checks(events, httpx_mock): assert ("active" in flags and "passive" not in flags) or ("active" not in flags and "passive" in flags), ( f'module "{module_name}" must have either "active" or "passive" flag' ) - assert ("safe" in flags and "aggressive" not in flags) or ( - "safe" not in flags and "aggressive" in flags - ), f'module "{module_name}" must have either "safe" or "aggressive" flag' - assert not ("web-basic" in flags and "web-thorough" in flags), ( - f'module "{module_name}" should have either "web-basic" or "web-thorough" flags, not both' + assert not ("web" in flags and "web-heavy" in flags), ( + f'module "{module_name}" should have either "web" or "web-heavy" flags, not both' + ) + # every scan module must be classified as safe, loud, and/or invasive + has_safe = "safe" in flags + has_loud = "loud" in flags + has_invasive = "invasive" in flags + assert has_safe or has_loud or has_invasive, ( + f'module "{module_name}" must have at least one of "safe", "loud", or "invasive" flags' + ) + assert not (has_safe and (has_loud or has_invasive)), ( + f'module "{module_name}" has "safe" flag but also has "loud" or "invasive" — these are mutually exclusive' ) meta = preloaded.get("meta", {}) # make sure every module has a description @@ -238,11 +274,13 @@ class mod_domain_only(BaseModule): force_start=True, ) + await scan._prep() + scan.modules["mod_normal"] = mod_normal(scan) scan.modules["mod_host_only"] = mod_host_only(scan) scan.modules["mod_hostport_only"] = mod_hostport_only(scan) scan.modules["mod_domain_only"] = mod_domain_only(scan) - scan.status = "RUNNING" + await scan._set_status("RUNNING") url_1 = scan.make_event("http://evilcorp.com/1", event_type="URL", parent=scan.root_event, tags=["status-200"]) url_2 = scan.make_event("http://evilcorp.com/2", event_type="URL", parent=scan.root_event, tags=["status-200"]) @@ -308,9 +346,9 @@ async def test_modules_basic_perdomainonly(bbot_scanner, monkeypatch): force_start=True, ) - await per_domain_scan.load_modules() + await per_domain_scan._prep() await per_domain_scan.setup_modules() - per_domain_scan.status = "RUNNING" + await per_domain_scan._set_status("RUNNING") # ensure that multiple events to the same "host" (schema + host) are blocked and check the per host tracker @@ -379,7 +417,16 @@ async def handle_event(self, event): # quick emit events like FINDINGS behave differently than normal ones # hosts are not speculated from them await self.emit_event( - {"host": "www.evilcorp.com", "url": "http://www.evilcorp.com", "description": "asdf"}, "FINDING", event + { + "host": "www.evilcorp.com", + "url": "http://www.evilcorp.com", + "description": "asdf", + "name": "Finding", + "severity": "LOW", + "confidence": "MEDIUM", + }, + "FINDING", + event, ) await self.emit_event("https://asdf.evilcorp.com", "URL", event, tags=["status-200"]) @@ -389,6 +436,9 @@ async def handle_event(self, event): output_modules=["python"], force_start=True, ) + + await scan._prep() + await scan.helpers.dns._mock_dns( { "evilcorp.com": {"A": ["127.0.254.1"]}, @@ -422,9 +472,9 @@ async def handle_event(self, event): "FINDING": 1, } - assert set(scan.stats.module_stats) == {"speculate", "host", "TARGET", "python", "dummy", "dnsresolve"} + assert set(scan.stats.module_stats) == {"speculate", "host", "SEED", "python", "dummy", "dnsresolve"} - target_stats = scan.stats.module_stats["TARGET"] + target_stats = scan.stats.module_stats["SEED"] assert target_stats.produced == {"SCAN": 1, "DNS_NAME": 1} assert target_stats.produced_total == 2 assert target_stats.consumed == {} @@ -464,6 +514,8 @@ async def handle_event(self, event): assert speculate_stats.consumed == {"URL": 1, "DNS_NAME": 3, "URL_UNVERIFIED": 1, "IP_ADDRESS": 3} assert speculate_stats.consumed_total == 8 + await scan._cleanup() + @pytest.mark.asyncio async def test_module_loading(bbot_scanner): @@ -473,8 +525,8 @@ async def test_module_loading(bbot_scanner): config={i: True for i in available_internal_modules if i != "dnsresolve"}, force_start=True, ) - await scan2.load_modules() - scan2.status = "RUNNING" + await scan2._prep() + await scan2._set_status("RUNNING") # attributes, descriptions, etc. for module_name, module in sorted(scan2.modules.items()): diff --git a/bbot/test/test_step_1/test_preset_seeds.py b/bbot/test/test_step_1/test_preset_seeds.py new file mode 100644 index 0000000000..07d74c2d9c --- /dev/null +++ b/bbot/test/test_step_1/test_preset_seeds.py @@ -0,0 +1,25 @@ +from bbot.scanner.preset import Preset + + +def test_preset_target_and_seeds_default(): + """ + If no explicit seeds are provided, seeds should be copied from target. + """ + preset = Preset("evilcorp.com") + baked = preset.bake() + + target = baked.target + assert set(target.target.inputs) == {"evilcorp.com"} + assert set(target.seeds.inputs) == {"evilcorp.com"} + + +def test_preset_target_and_seeds_explicit_seeds_override(): + """ + If explicit seeds are provided, they should NOT be copied from target. + """ + preset = Preset("evilcorp.com", seeds=["seedonly.evilcorp.com"]) + baked = preset.bake() + + target = baked.target + assert set(target.target.inputs) == {"evilcorp.com"} + assert set(target.seeds.inputs) == {"seedonly.evilcorp.com"} diff --git a/bbot/test/test_step_1/test_presets.py b/bbot/test/test_step_1/test_presets.py index 82b1638752..ff8e4d0214 100644 --- a/bbot/test/test_step_1/test_presets.py +++ b/bbot/test/test_step_1/test_presets.py @@ -67,13 +67,12 @@ def test_core(): assert "test456" in core_copy.config["test123"] -def test_preset_yaml(clean_default_config): +async def test_preset_yaml(clean_default_config): import yaml preset1 = Preset( - "evilcorp.com", - "www.evilcorp.ce", - whitelist=["evilcorp.ce"], + "evilcorp.ce", + seeds=["evilcorp.com", "www.evilcorp.ce"], blacklist=["test.www.evilcorp.ce"], modules=["sslcert"], output_modules=["json"], @@ -90,14 +89,14 @@ def test_preset_yaml(clean_default_config): assert "evilcorp.com" in preset1.target.seeds assert "evilcorp.ce" not in preset1.target.seeds assert "asdf.www.evilcorp.ce" in preset1.target.seeds - assert "evilcorp.ce" in preset1.whitelist - assert "asdf.evilcorp.ce" in preset1.whitelist + assert "evilcorp.ce" in preset1.target.target + assert "asdf.evilcorp.ce" in preset1.target.target assert "test.www.evilcorp.ce" in preset1.blacklist assert "asdf.test.www.evilcorp.ce" in preset1.blacklist assert "sslcert" in preset1.scan_modules - assert preset1.whitelisted("evilcorp.ce") - assert preset1.whitelisted("www.evilcorp.ce") - assert not preset1.whitelisted("evilcorp.com") + assert preset1.in_target("evilcorp.ce") + assert preset1.in_target("www.evilcorp.ce") + assert not preset1.in_target("evilcorp.com") assert preset1.blacklisted("test.www.evilcorp.ce") assert preset1.blacklisted("asdf.test.www.evilcorp.ce") assert not preset1.blacklisted("www.evilcorp.ce") @@ -113,7 +112,7 @@ def test_preset_yaml(clean_default_config): - subdomain-enum exclude_flags: - - aggressive + - loud - slow require_flags: @@ -152,7 +151,7 @@ def test_preset_cache(): - subdomain-enum exclude_flags: - - aggressive + - loud - slow """ with open(preset_file, "w") as f: @@ -160,7 +159,7 @@ def test_preset_cache(): preset = Preset.from_yaml_file(preset_file) assert "subdomain-enum" in preset.flags - assert "aggressive" in preset.exclude_flags + assert "loud" in preset.exclude_flags assert "slow" in preset.exclude_flags from bbot.scanner.preset.preset import _preset_cache @@ -169,34 +168,36 @@ def test_preset_cache(): preset_file.unlink() -def test_preset_scope(): +@pytest.mark.asyncio +async def test_preset_scope(clean_default_config): # test target merging scan = Scanner("1.2.3.4", preset=Preset.from_dict({"target": ["evilcorp.com"]})) + await scan._prep() assert {str(h) for h in scan.preset.target.seeds.hosts} == {"1.2.3.4/32", "evilcorp.com"} assert {e.data for e in scan.target.seeds} == {"1.2.3.4", "evilcorp.com"} - assert {e.data for e in scan.target.whitelist} == {"1.2.3.4/32", "evilcorp.com"} + assert {str(h) for h in scan.target.target.hosts} == {"1.2.3.4/32", "evilcorp.com"} blank_preset = Preset() blank_preset = blank_preset.bake() assert not blank_preset.target.seeds - assert not blank_preset.target.whitelist + assert not blank_preset.target.target assert blank_preset.strict_scope is False + # Positional args define target; seeds must be explicit preset1 = Preset( - "evilcorp.com", - "www.evilcorp.ce", - whitelist=["evilcorp.ce"], + "evilcorp.ce", + seeds=["evilcorp.com", "www.evilcorp.ce"], blacklist=["test.www.evilcorp.ce"], ) preset1_baked = preset1.bake() # make sure target logic works as expected assert "evilcorp.com" in preset1_baked.target.seeds - assert "evilcorp.com" not in preset1_baked.target.whitelist + assert "evilcorp.com" not in preset1_baked.target.target assert "asdf.evilcorp.com" in preset1_baked.target.seeds - assert "asdf.evilcorp.com" not in preset1_baked.target.whitelist - assert "asdf.evilcorp.ce" in preset1_baked.whitelist - assert "evilcorp.ce" in preset1_baked.whitelist + assert "asdf.evilcorp.com" not in preset1_baked.target.target + assert "asdf.evilcorp.ce" in preset1_baked.target.target + assert "evilcorp.ce" in preset1_baked.target.target assert "test.www.evilcorp.ce" in preset1_baked.blacklist assert "evilcorp.ce" not in preset1_baked.blacklist assert preset1_baked.in_scope("www.evilcorp.ce") @@ -211,8 +212,8 @@ def test_preset_scope(): # test preset merging preset3 = Preset( - "evilcorp.org", - whitelist=["evilcorp.de"], + "evilcorp.de", + seeds=["evilcorp.org"], blacklist=["test.www.evilcorp.de"], config={"scope": {"strict": True}}, ) @@ -230,10 +231,10 @@ def test_preset_scope(): assert "asdf.evilcorp.org" not in preset1_baked.target.seeds assert "asdf.evilcorp.com" not in preset1_baked.target.seeds assert "asdf.www.evilcorp.ce" not in preset1_baked.target.seeds - assert "evilcorp.ce" in preset1_baked.whitelist - assert "evilcorp.de" in preset1_baked.whitelist - assert "asdf.evilcorp.de" not in preset1_baked.whitelist - assert "asdf.evilcorp.ce" not in preset1_baked.whitelist + assert "evilcorp.ce" in preset1_baked.target.target + assert "evilcorp.de" in preset1_baked.target.target + assert "asdf.evilcorp.de" not in preset1_baked.target.target + assert "asdf.evilcorp.ce" not in preset1_baked.target.target # blacklist should be merged, strict scope does not apply assert "test.www.evilcorp.ce" in preset1_baked.blacklist assert "test.www.evilcorp.de" in preset1_baked.blacklist @@ -253,130 +254,156 @@ def test_preset_scope(): preset1.merge(preset4) set(preset1.output_modules) == {"python", "csv", "txt", "json", "stdout", "neo4j"} - # test preset merging + whitelist + # test preset merging + seeds/target interaction - preset_nowhitelist = Preset("evilcorp.com", name="nowhitelist") - preset_whitelist = Preset( - "evilcorp.org", - name="whitelist", - whitelist=["1.2.3.4/24", "http://evilcorp.net"], + # Domain present as both explicit seed and targets + preset_domain_with_seed = Preset("evilcorp.com", seeds=["evilcorp.com"], name="domain_with_seed") + preset_with_target_scope = Preset( + "1.2.3.4/24", + "http://evilcorp.net", + name="with_target_scope", + seeds=["evilcorp.org"], blacklist=["evilcorp.co.uk:443", "bob@evilcorp.co.uk"], config={"modules": {"secretsdb": {"api_key": "deadbeef", "otherthing": "asdf"}}}, ) - preset_nowhitelist_baked = preset_nowhitelist.bake() - preset_whitelist_baked = preset_whitelist.bake() - - assert preset_nowhitelist_baked.to_dict(include_target=True) == { - "target": ["evilcorp.com"], + preset_domain_with_seed_baked = preset_domain_with_seed.bake() + preset_with_target_scope_baked = preset_with_target_scope.bake() + + # When seeds and targets are identical, only targets are serialized. + domain_with_seed_dict = preset_domain_with_seed_baked.to_dict(include_target=True) + assert domain_with_seed_dict.get("target") == ["evilcorp.com"] + assert "seeds" not in domain_with_seed_dict + + # preset with explicit target scope + scope_dict = preset_with_target_scope_baked.to_dict(include_target=True) + assert set(scope_dict["target"]) == {"1.2.3.0/24", "http://evilcorp.net/"} + assert set(scope_dict["blacklist"]) == {"bob@evilcorp.co.uk", "evilcorp.co.uk:443"} + # secretsdb config should be preserved (other module config may also be present) + assert scope_dict["config"]["modules"]["secretsdb"] == { + "api_key": "deadbeef", + "otherthing": "asdf", } - assert preset_whitelist_baked.to_dict(include_target=True) == { - "target": ["evilcorp.org"], - "whitelist": ["1.2.3.0/24", "http://evilcorp.net/"], - "blacklist": ["bob@evilcorp.co.uk", "evilcorp.co.uk:443"], - "config": {"modules": {"secretsdb": {"api_key": "deadbeef", "otherthing": "asdf"}}}, + + redacted_dict = preset_with_target_scope_baked.to_dict(include_target=True, redact_secrets=True) + assert set(redacted_dict["target"]) == {"1.2.3.0/24", "http://evilcorp.net/"} + assert set(redacted_dict["blacklist"]) == {"bob@evilcorp.co.uk", "evilcorp.co.uk:443"} + assert redacted_dict["config"]["modules"]["secretsdb"] == {"otherthing": "asdf"} + + assert preset_domain_with_seed_baked.in_scope("www.evilcorp.com") + assert not preset_domain_with_seed_baked.in_scope("www.evilcorp.de") + assert not preset_domain_with_seed_baked.in_scope("1.2.3.4/24") + + assert "www.evilcorp.org" in preset_with_target_scope_baked.target.seeds + assert "www.evilcorp.org" not in preset_with_target_scope_baked.target.target + assert "1.2.3.4" in preset_with_target_scope_baked.target.target + assert not preset_with_target_scope_baked.in_scope("www.evilcorp.org") + assert not preset_with_target_scope_baked.in_scope("www.evilcorp.de") + assert not preset_with_target_scope_baked.in_target("www.evilcorp.org") + assert not preset_with_target_scope_baked.in_target("www.evilcorp.de") + assert preset_with_target_scope_baked.in_scope("1.2.3.4") + assert preset_with_target_scope_baked.in_scope("1.2.3.4/28") + assert preset_with_target_scope_baked.in_scope("1.2.3.4/24") + assert preset_with_target_scope_baked.in_target("1.2.3.4") + assert preset_with_target_scope_baked.in_target("1.2.3.4/28") + assert preset_with_target_scope_baked.in_target("1.2.3.4/24") + + assert {e.data for e in preset_domain_with_seed_baked.seeds} == {"evilcorp.com"} + assert {e.data for e in preset_domain_with_seed_baked.target.target} == {"evilcorp.com"} + assert {e.data for e in preset_with_target_scope_baked.seeds} == {"evilcorp.org"} + assert {e.data for e in preset_with_target_scope_baked.target.target} == {"1.2.3.0/24", "http://evilcorp.net/"} + + # When merging a preset that has both seeds and target with one that only has + # target (no explicit seeds), explicit seeds are unioned and targets are unioned. + preset_domain_with_seed.merge(preset_with_target_scope) + preset_domain_with_seed_baked = preset_domain_with_seed.bake() + assert {e.data for e in preset_domain_with_seed_baked.seeds} == {"evilcorp.com", "evilcorp.org"} + # After merging, target scope should include both the original domain target and the scoped network/URL + assert {e.data for e in preset_domain_with_seed_baked.target.target} == { + "evilcorp.com", + "1.2.3.0/24", + "http://evilcorp.net/", } - assert preset_whitelist_baked.to_dict(include_target=True, redact_secrets=True) == { - "target": ["evilcorp.org"], - "whitelist": ["1.2.3.0/24", "http://evilcorp.net/"], - "blacklist": ["bob@evilcorp.co.uk", "evilcorp.co.uk:443"], - "config": {"modules": {"secretsdb": {"otherthing": "asdf"}}}, + assert "www.evilcorp.org" in preset_domain_with_seed_baked.seeds + assert "www.evilcorp.com" in preset_domain_with_seed_baked.seeds + assert "1.2.3.4" in preset_domain_with_seed_baked.target.target + assert not preset_domain_with_seed_baked.in_scope("www.evilcorp.org") + # After merging, evilcorp.com remains in target, so its www subdomain is in-scope and in-target + assert preset_domain_with_seed_baked.in_scope("www.evilcorp.com") + assert not preset_domain_with_seed_baked.in_target("www.evilcorp.org") + assert preset_domain_with_seed_baked.in_target("www.evilcorp.com") + assert preset_domain_with_seed_baked.in_scope("1.2.3.4") + + # When merging a preset that only defines targets (no explicit seeds), + # its targets are not promoted to seeds in the merged preset, but targets are unioned. + preset_targets_only = Preset("evilcorp.com") + preset_with_target_scope = Preset("1.2.3.4/24", seeds=["evilcorp.org"]) + preset_with_target_scope.merge(preset_targets_only) + preset_with_target_scope_baked = preset_with_target_scope.bake() + # Seeds stay as the explicit seeds from the base preset + assert {e.data for e in preset_with_target_scope_baked.seeds} == {"evilcorp.org"} + # Target scope is the union of both presets' targets. + assert {e.data for e in preset_with_target_scope_baked.target.target} == { + "evilcorp.com", + "1.2.3.0/24", } - - assert preset_nowhitelist_baked.in_scope("www.evilcorp.com") - assert not preset_nowhitelist_baked.in_scope("www.evilcorp.de") - assert not preset_nowhitelist_baked.in_scope("1.2.3.4/24") - - assert "www.evilcorp.org" in preset_whitelist_baked.target.seeds - assert "www.evilcorp.org" not in preset_whitelist_baked.target.whitelist - assert "1.2.3.4" in preset_whitelist_baked.whitelist - assert not preset_whitelist_baked.in_scope("www.evilcorp.org") - assert not preset_whitelist_baked.in_scope("www.evilcorp.de") - assert not preset_whitelist_baked.whitelisted("www.evilcorp.org") - assert not preset_whitelist_baked.whitelisted("www.evilcorp.de") - assert preset_whitelist_baked.in_scope("1.2.3.4") - assert preset_whitelist_baked.in_scope("1.2.3.4/28") - assert preset_whitelist_baked.in_scope("1.2.3.4/24") - assert preset_whitelist_baked.whitelisted("1.2.3.4") - assert preset_whitelist_baked.whitelisted("1.2.3.4/28") - assert preset_whitelist_baked.whitelisted("1.2.3.4/24") - - assert {e.data for e in preset_nowhitelist_baked.seeds} == {"evilcorp.com"} - assert {e.data for e in preset_nowhitelist_baked.whitelist} == {"evilcorp.com"} - assert {e.data for e in preset_whitelist_baked.seeds} == {"evilcorp.org"} - assert {e.data for e in preset_whitelist_baked.whitelist} == {"1.2.3.0/24", "http://evilcorp.net/"} - - preset_nowhitelist.merge(preset_whitelist) - preset_nowhitelist_baked = preset_nowhitelist.bake() - assert {e.data for e in preset_nowhitelist_baked.seeds} == {"evilcorp.com", "evilcorp.org"} - assert {e.data for e in preset_nowhitelist_baked.whitelist} == {"1.2.3.0/24", "http://evilcorp.net/"} - assert "www.evilcorp.org" in preset_nowhitelist_baked.seeds - assert "www.evilcorp.com" in preset_nowhitelist_baked.seeds - assert "1.2.3.4" in preset_nowhitelist_baked.whitelist - assert not preset_nowhitelist_baked.in_scope("www.evilcorp.org") - assert not preset_nowhitelist_baked.in_scope("www.evilcorp.com") - assert not preset_nowhitelist_baked.whitelisted("www.evilcorp.org") - assert not preset_nowhitelist_baked.whitelisted("www.evilcorp.com") - assert preset_nowhitelist_baked.in_scope("1.2.3.4") - - preset_nowhitelist = Preset("evilcorp.com") - preset_whitelist = Preset("evilcorp.org", whitelist=["1.2.3.4/24"]) - preset_whitelist.merge(preset_nowhitelist) - preset_whitelist_baked = preset_whitelist.bake() - assert {e.data for e in preset_whitelist_baked.seeds} == {"evilcorp.com", "evilcorp.org"} - assert {e.data for e in preset_whitelist_baked.whitelist} == {"1.2.3.0/24"} - assert "www.evilcorp.org" in preset_whitelist_baked.seeds - assert "www.evilcorp.com" in preset_whitelist_baked.seeds - assert "www.evilcorp.org" not in preset_whitelist_baked.target.whitelist - assert "www.evilcorp.com" not in preset_whitelist_baked.target.whitelist - assert "1.2.3.4" in preset_whitelist_baked.whitelist - assert not preset_whitelist_baked.in_scope("www.evilcorp.org") - assert not preset_whitelist_baked.in_scope("www.evilcorp.com") - assert not preset_whitelist_baked.whitelisted("www.evilcorp.org") - assert not preset_whitelist_baked.whitelisted("www.evilcorp.com") - assert preset_whitelist_baked.in_scope("1.2.3.4") - - preset_nowhitelist1 = Preset("evilcorp.com") - preset_nowhitelist2 = Preset("evilcorp.de") - preset_nowhitelist1_baked = preset_nowhitelist1.bake() - preset_nowhitelist2_baked = preset_nowhitelist2.bake() - assert {e.data for e in preset_nowhitelist1_baked.seeds} == {"evilcorp.com"} - assert {e.data for e in preset_nowhitelist2_baked.seeds} == {"evilcorp.de"} - assert {e.data for e in preset_nowhitelist1_baked.whitelist} == {"evilcorp.com"} - assert {e.data for e in preset_nowhitelist2_baked.whitelist} == {"evilcorp.de"} - preset_nowhitelist1.merge(preset_nowhitelist2) - preset_nowhitelist1_baked = preset_nowhitelist1.bake() - assert {e.data for e in preset_nowhitelist1_baked.seeds} == {"evilcorp.com", "evilcorp.de"} - assert {e.data for e in preset_nowhitelist2_baked.seeds} == {"evilcorp.de"} - assert {e.data for e in preset_nowhitelist1_baked.whitelist} == {"evilcorp.com", "evilcorp.de"} - assert {e.data for e in preset_nowhitelist2_baked.whitelist} == {"evilcorp.de"} - assert "www.evilcorp.com" in preset_nowhitelist1_baked.seeds - assert "www.evilcorp.de" in preset_nowhitelist1_baked.seeds - assert "www.evilcorp.com" in preset_nowhitelist1_baked.target.seeds - assert "www.evilcorp.de" in preset_nowhitelist1_baked.target.seeds - assert "www.evilcorp.com" in preset_nowhitelist1_baked.whitelist - assert "www.evilcorp.de" in preset_nowhitelist1_baked.whitelist - assert preset_nowhitelist1_baked.whitelisted("www.evilcorp.com") - assert preset_nowhitelist1_baked.whitelisted("www.evilcorp.de") - assert not preset_nowhitelist1_baked.whitelisted("1.2.3.4") - assert preset_nowhitelist1_baked.in_scope("www.evilcorp.com") - assert preset_nowhitelist1_baked.in_scope("www.evilcorp.de") - assert not preset_nowhitelist1_baked.in_scope("1.2.3.4") - - preset_nowhitelist1 = Preset("evilcorp.com") - preset_nowhitelist2 = Preset("evilcorp.de") - preset_nowhitelist2.merge(preset_nowhitelist1) - preset_nowhitelist1_baked = preset_nowhitelist1.bake() - preset_nowhitelist2_baked = preset_nowhitelist2.bake() - assert {e.data for e in preset_nowhitelist1_baked.seeds} == {"evilcorp.com"} - assert {e.data for e in preset_nowhitelist2_baked.seeds} == {"evilcorp.com", "evilcorp.de"} - assert {e.data for e in preset_nowhitelist1_baked.whitelist} == {"evilcorp.com"} - assert {e.data for e in preset_nowhitelist2_baked.whitelist} == {"evilcorp.com", "evilcorp.de"} + # Seed expansion only applies to explicit seeds (evilcorp.org), not merged targets. + assert "www.evilcorp.org" in preset_with_target_scope_baked.seeds + assert "www.evilcorp.com" not in preset_with_target_scope_baked.seeds + # Target expansion only applies to targets (evilcorp.com), not seeds-only domains. + assert "www.evilcorp.org" not in preset_with_target_scope_baked.target.target + assert "www.evilcorp.com" in preset_with_target_scope_baked.target.target + # Scope/target checks reflect that only evilcorp.com is in the merged target. + assert not preset_with_target_scope_baked.in_scope("www.evilcorp.org") + assert preset_with_target_scope_baked.in_scope("www.evilcorp.com") + assert not preset_with_target_scope_baked.in_target("www.evilcorp.org") + assert preset_with_target_scope_baked.in_target("www.evilcorp.com") + assert preset_with_target_scope_baked.in_scope("1.2.3.4") + + # Merging two presets created only with positional targets: + # after bake, each has seeds backfilled from its own target, and merge unions both. + preset_targets_only1 = Preset("evilcorp.com") + preset_targets_only2 = Preset("evilcorp.de") + preset_targets_only1_baked = preset_targets_only1.bake() + preset_targets_only2_baked = preset_targets_only2.bake() + assert {e.data for e in preset_targets_only1_baked.seeds} == {"evilcorp.com"} + assert {e.data for e in preset_targets_only2_baked.seeds} == {"evilcorp.de"} + assert {e.data for e in preset_targets_only1_baked.target.target} == {"evilcorp.com"} + assert {e.data for e in preset_targets_only2_baked.target.target} == {"evilcorp.de"} + preset_targets_only1.merge(preset_targets_only2) + preset_targets_only1_baked = preset_targets_only1.bake() + assert {e.data for e in preset_targets_only1_baked.seeds} == {"evilcorp.com", "evilcorp.de"} + assert {e.data for e in preset_targets_only2_baked.seeds} == {"evilcorp.de"} + assert {e.data for e in preset_targets_only1_baked.target.target} == {"evilcorp.com", "evilcorp.de"} + assert {e.data for e in preset_targets_only2_baked.target.target} == {"evilcorp.de"} + assert "www.evilcorp.com" in preset_targets_only1_baked.seeds + assert "www.evilcorp.de" in preset_targets_only1_baked.seeds + assert "www.evilcorp.com" in preset_targets_only1_baked.target.seeds + assert "www.evilcorp.de" in preset_targets_only1_baked.target.seeds + assert "www.evilcorp.com" in preset_targets_only1_baked.target.target + assert "www.evilcorp.de" in preset_targets_only1_baked.target.target + assert preset_targets_only1_baked.in_target("www.evilcorp.com") + assert preset_targets_only1_baked.in_target("www.evilcorp.de") + assert not preset_targets_only1_baked.in_target("1.2.3.4") + assert preset_targets_only1_baked.in_scope("www.evilcorp.com") + assert preset_targets_only1_baked.in_scope("www.evilcorp.de") + assert not preset_targets_only1_baked.in_scope("1.2.3.4") + + preset_targets_only1 = Preset("evilcorp.com") + preset_targets_only2 = Preset("evilcorp.de") + preset_targets_only2.merge(preset_targets_only1) + preset_targets_only1_baked = preset_targets_only1.bake() + preset_targets_only2_baked = preset_targets_only2.bake() + assert {e.data for e in preset_targets_only1_baked.seeds} == {"evilcorp.com"} + assert {e.data for e in preset_targets_only2_baked.seeds} == {"evilcorp.com", "evilcorp.de"} + assert {e.data for e in preset_targets_only1_baked.target.target} == {"evilcorp.com"} + assert {e.data for e in preset_targets_only2_baked.target.target} == {"evilcorp.com", "evilcorp.de"} @pytest.mark.asyncio async def test_preset_logging(): scan = Scanner() + await scan._prep() # test individual verbosity levels original_log_level = CORE.logger.log_level @@ -475,7 +502,7 @@ async def test_preset_logging(): await scan._cleanup() -def test_preset_module_resolution(clean_default_config): +async def test_preset_module_resolution(clean_default_config): preset = Preset().bake() sslcert_preloaded = preset.preloaded_module("sslcert") wayback_preloaded = preset.preloaded_module("wayback") @@ -549,8 +576,7 @@ def test_preset_module_resolution(clean_default_config): assert set(preset.scan_modules) == {"wayback"} # modules + module exclusions - preset = Preset(exclude_modules=["sslcert"], modules=["sslcert", "dotnetnuke", "wayback"]).bake() - baked_preset = preset.bake() + baked_preset = Preset(exclude_modules=["sslcert"], modules=["sslcert", "dotnetnuke", "wayback"]).bake() assert baked_preset.modules == { "wayback", "cloudcheck", @@ -568,6 +594,81 @@ def test_preset_module_resolution(clean_default_config): } +@pytest.mark.asyncio +async def test_custom_module_dir(): + custom_module_dir = bbot_test_dir / "custom_modules" + custom_module_dir.mkdir(parents=True, exist_ok=True) + + custom_module = custom_module_dir / "testmodule.py" + with open(custom_module, "w") as f: + f.write( + """ +from bbot.modules.base import BaseModule + +class TestModule(BaseModule): + watched_events = ["SCAN"] + + async def handle_event(self, event): + await self.emit_event("127.0.0.2", parent=event) +""" + ) + + preset = { + "module_dirs": [str(custom_module_dir)], + "modules": ["testmodule"], + } + preset = Preset.from_dict(preset) + + scan = Scanner("127.0.0.0/24", preset=preset) + events = [e async for e in scan.async_start()] + event_data = [(str(e.data), str(e.module)) for e in events] + assert ("127.0.0.2", "testmodule") in event_data + + shutil.rmtree(custom_module_dir) + + +def test_preset_scope_round_trip(clean_default_config): + preset_dict = { + # seeds: initial inputs that drive passive modules + "seeds": ["127.0.0.1"], + # target: what in_target() / in_scope() check + "target": ["127.0.0.2"], + "blacklist": ["127.0.0.3"], + "config": {"scope": {"strict": True}}, + } + preset = Preset.from_dict(preset_dict) + baked = preset.bake() + # Seeds should round-trip unchanged + assert list(baked.seeds) == ["127.0.0.1"] + # Target list should round-trip unchanged + assert list(baked.target.target.inputs) == ["127.0.0.2"] + # Blacklist should round-trip unchanged + assert list(baked.blacklist) == ["127.0.0.3"] + # Scope config should be preserved + result = baked.to_dict(include_target=True) + assert result["config"]["scope"] == preset_dict["config"]["scope"] + + +def test_preset_target_tolerance(): + # tolerate both "target" and "targets", since this is a common oopsie + preset_dict = { + "target": ["127.0.0.1"], + "targets": ["127.0.0.2"], + } + preset = Preset.from_dict(preset_dict) + baked = preset.bake() + assert set(baked.seeds) == {"127.0.0.1", "127.0.0.2"} + + preset = Preset.from_yaml_string(""" +target: + - 127.0.0.1 +targets: + - 127.0.0.2 +""") + baked = preset.bake() + assert set(baked.seeds) == {"127.0.0.1", "127.0.0.2"} + + @pytest.mark.asyncio async def test_preset_module_loader(): custom_module_dir = bbot_test_dir / "custom_module_dir" @@ -585,7 +686,7 @@ async def test_preset_module_loader(): class TestModule1(BaseModule): watched_events = ["URL", "HTTP_RESPONSE"] - produced_events = ["VULNERABILITY"] + produced_events = ["FINDING"] """ ) @@ -701,6 +802,7 @@ class TestModule5(BaseModule): # should fail with pytest.raises(ValidationError): scan = Scanner(preset=preset) + await scan._prep() preset = Preset.from_yaml_string( f""" @@ -851,6 +953,7 @@ async def test_preset_conditions(): assert preset.conditions scan = Scanner(preset=preset) + await scan._prep() assert scan.preset.conditions await scan._cleanup() @@ -859,10 +962,11 @@ async def test_preset_conditions(): preset.merge(preset2) with pytest.raises(PresetAbortError): - Scanner(preset=preset) + scan = Scanner(preset=preset) + await scan._prep() -def test_preset_module_disablement(clean_default_config): +async def test_preset_module_disablement(clean_default_config): # internal module disablement preset = Preset().bake() assert "speculate" in preset.internal_modules @@ -886,7 +990,7 @@ def test_preset_module_disablement(clean_default_config): assert set(preset.output_modules) == {"json"} -def test_preset_override(): +async def test_preset_override(clean_default_config): # tests to make sure a preset's config settings override others it includes preset_1_yaml = """ name: override1 @@ -968,7 +1072,7 @@ def test_preset_override(): assert set(preset.scan_modules) == {"httpx", "c99", "robots", "virustotal", "securitytrails"} -def test_preset_require_exclude(): +async def test_preset_require_exclude(clean_default_config): def get_module_flags(p): for m in p.scan_modules: preloaded = p.preloaded_module(m) @@ -982,7 +1086,7 @@ def get_module_flags(p): assert "subdomain-enum" in dnsbrute_flags assert "active" in dnsbrute_flags assert "passive" not in dnsbrute_flags - assert "aggressive" in dnsbrute_flags + assert "loud" in dnsbrute_flags assert "safe" not in dnsbrute_flags assert "dnsbrute" in [x[0] for x in module_flags] assert "certspotter" in [x[0] for x in module_flags] @@ -990,7 +1094,7 @@ def get_module_flags(p): assert any("passive" in flags for module, flags in module_flags) assert any("active" in flags for module, flags in module_flags) assert any("safe" in flags for module, flags in module_flags) - assert any("aggressive" in flags for module, flags in module_flags) + assert any("loud" in flags for module, flags in module_flags) # enable by flag, one required flag preset = Preset(flags=["subdomain-enum"], require_flags=["passive"]).bake() @@ -1001,7 +1105,7 @@ def get_module_flags(p): assert all("passive" in flags for module, flags in module_flags) assert not any("active" in flags for module, flags in module_flags) assert any("safe" in flags for module, flags in module_flags) - assert any("aggressive" in flags for module, flags in module_flags) + assert any("loud" in flags for module, flags in module_flags) # enable by flag, one excluded flag preset = Preset(flags=["subdomain-enum"], exclude_flags=["active"]).bake() @@ -1012,7 +1116,7 @@ def get_module_flags(p): assert all("passive" in flags for module, flags in module_flags) assert not any("active" in flags for module, flags in module_flags) assert any("safe" in flags for module, flags in module_flags) - assert any("aggressive" in flags for module, flags in module_flags) + assert any("loud" in flags for module, flags in module_flags) # enable by flag, one excluded module preset = Preset(flags=["subdomain-enum"], exclude_modules=["dnsbrute"]).bake() @@ -1023,27 +1127,27 @@ def get_module_flags(p): assert any("passive" in flags for module, flags in module_flags) assert any("active" in flags for module, flags in module_flags) assert any("safe" in flags for module, flags in module_flags) - assert any("aggressive" in flags for module, flags in module_flags) + assert any("loud" in flags for module, flags in module_flags) # enable by flag, multiple required flags preset = Preset(flags=["subdomain-enum"], require_flags=["safe", "passive"]).bake() - assert len(preset.modules) > 25 + assert len(preset.modules) > 20 module_flags = list(get_module_flags(preset)) assert "dnsbrute" not in [x[0] for x in module_flags] assert all("passive" in flags and "safe" in flags for module, flags in module_flags) - assert all("active" not in flags and "aggressive" not in flags for module, flags in module_flags) + assert all("active" not in flags and "loud" not in flags for module, flags in module_flags) assert not any("active" in flags for module, flags in module_flags) - assert not any("aggressive" in flags for module, flags in module_flags) + assert not any("loud" in flags for module, flags in module_flags) # enable by flag, multiple excluded flags - preset = Preset(flags=["subdomain-enum"], exclude_flags=["aggressive", "active"]).bake() - assert len(preset.modules) > 25 + preset = Preset(flags=["subdomain-enum"], exclude_flags=["loud", "active"]).bake() + assert len(preset.modules) > 20 module_flags = list(get_module_flags(preset)) assert "dnsbrute" not in [x[0] for x in module_flags] assert all("passive" in flags and "safe" in flags for module, flags in module_flags) - assert all("active" not in flags and "aggressive" not in flags for module, flags in module_flags) + assert all("active" not in flags and "loud" not in flags for module, flags in module_flags) assert not any("active" in flags for module, flags in module_flags) - assert not any("aggressive" in flags for module, flags in module_flags) + assert not any("loud" in flags for module, flags in module_flags) # enable by flag, multiple excluded modules preset = Preset(flags=["subdomain-enum"], exclude_modules=["dnsbrute", "c99"]).bake() @@ -1055,7 +1159,7 @@ def get_module_flags(p): assert any("passive" in flags for module, flags in module_flags) assert any("active" in flags for module, flags in module_flags) assert any("safe" in flags for module, flags in module_flags) - assert any("aggressive" in flags for module, flags in module_flags) + assert any("loud" in flags for module, flags in module_flags) @pytest.mark.asyncio @@ -1078,7 +1182,7 @@ async def test_preset_output_dir(): # regression test for https://github.com/blacklanternsecurity/bbot/issues/2337 -def test_preset_serialization(): +async def test_preset_serialization(clean_default_config): preset = Preset("192.168.1.1") preset = preset.bake() @@ -1087,5 +1191,86 @@ def test_preset_serialization(): preset_dict = preset.to_dict(include_target=True) print(preset_dict) preset_str = json.dumps(preset_dict) - preset_dict = json.loads(preset_str) - assert preset_dict == {"target": ["192.168.1.1"], "whitelist": ["192.168.1.1/32"]} + preset_dict_round_tripped = json.loads(preset_str) + assert preset_dict_round_tripped == preset_dict + assert preset_dict["target"] == ["192.168.1.1"] + assert "seeds" not in preset_dict + + +def test_preset_file_targets(tmp_path): + """Test that file paths in preset target/seeds/blacklist are resolved via PresetPath. + + The preset and its target files live in tmp_path (NOT CWD), so relative paths + like "targets.txt" can only be found if PresetPath adds the preset's directory + to its search paths. This is the core behavior being tested. + """ + import os + + # sanity check: tmp_path is not CWD (otherwise relative resolution is ambiguous) + assert os.getcwd() != str(tmp_path) + + # create target files next to where the preset will live + targets_file = tmp_path / "targets.txt" + targets_file.write_text("evilcorp.com\n1.2.3.4\n") + seeds_file = tmp_path / "seeds.txt" + seeds_file.write_text("seed1.evilcorp.com\nseed2.evilcorp.com\n") + blacklist_file = tmp_path / "blacklist.txt" + blacklist_file.write_text("internal.evilcorp.com\n10.0.0.0/8\n") + + # relative paths: resolved from the preset's directory via PresetPath + preset_file = tmp_path / "my_preset.yml" + preset_file.write_text("target:\n - targets.txt\nseeds:\n - seeds.txt\nblacklist:\n - blacklist.txt\n") + preset = Preset.from_yaml_file(str(preset_file)) + target_inputs = set(preset._target_list) + assert "evilcorp.com" in target_inputs + assert "1.2.3.4" in target_inputs + assert "targets.txt" not in target_inputs + seed_inputs = set(preset._seeds) + assert "seed1.evilcorp.com" in seed_inputs + assert "seed2.evilcorp.com" in seed_inputs + blacklist_inputs = set(preset._blacklist) + assert "internal.evilcorp.com" in blacklist_inputs + assert "10.0.0.0/8" in blacklist_inputs + + # absolute paths for targets, seeds, and blacklist + preset_file2 = tmp_path / "my_preset2.yml" + preset_file2.write_text( + f"target:\n - {targets_file}\nseeds:\n - {seeds_file}\nblacklist:\n - {blacklist_file}\n" + ) + preset2 = Preset.from_yaml_file(str(preset_file2)) + target_inputs2 = set(preset2._target_list) + assert "evilcorp.com" in target_inputs2 + assert "1.2.3.4" in target_inputs2 + seed_inputs2 = set(preset2._seeds) + assert "seed1.evilcorp.com" in seed_inputs2 + assert "seed2.evilcorp.com" in seed_inputs2 + blacklist_inputs2 = set(preset2._blacklist) + assert "internal.evilcorp.com" in blacklist_inputs2 + assert "10.0.0.0/8" in blacklist_inputs2 + + # mixed: file paths + literal targets + preset_file3 = tmp_path / "my_preset3.yml" + preset_file3.write_text("target:\n - targets.txt\n - extra.evilcorp.com\n") + preset3 = Preset.from_yaml_file(str(preset_file3)) + target_inputs3 = set(preset3._target_list) + assert "evilcorp.com" in target_inputs3 + assert "1.2.3.4" in target_inputs3 + assert "extra.evilcorp.com" in target_inputs3 + + # non-existent file strings are kept as literal targets + preset4 = Preset.from_dict({"target": ["not_a_file.txt", "192.168.1.1"]}) + target_inputs4 = set(preset4._target_list) + assert "not_a_file.txt" in target_inputs4 + assert "192.168.1.1" in target_inputs4 + + # subdirectory: preset in a nested dir references a file in the same nested dir + subdir = tmp_path / "nested" / "presets" + subdir.mkdir(parents=True) + nested_targets = subdir / "my_targets.txt" + nested_targets.write_text("nested.evilcorp.com\n") + nested_preset = subdir / "nested_preset.yml" + nested_preset.write_text("target:\n - my_targets.txt\n") + preset5 = Preset.from_yaml_file(str(nested_preset)) + target_inputs5 = set(preset5._target_list) + assert "nested.evilcorp.com" in target_inputs5 + assert "my_targets.txt" not in target_inputs5 diff --git a/bbot/test/test_step_1/test_python_api.py b/bbot/test/test_step_1/test_python_api.py index a915eea57d..0e0bb4c690 100644 --- a/bbot/test/test_step_1/test_python_api.py +++ b/bbot/test/test_step_1/test_python_api.py @@ -2,17 +2,19 @@ @pytest.mark.asyncio -async def test_python_api(): - from bbot import Scanner +async def test_python_api(clean_default_config): + from bbot.scanner import Scanner # make sure events are properly yielded scan1 = Scanner("127.0.0.1") + await scan1._prep() events1 = [] async for event in scan1.async_start(): events1.append(event) assert any(e.type == "IP_ADDRESS" and e.data == "127.0.0.1" for e in events1) # make sure output files work scan2 = Scanner("127.0.0.1", output_modules=["json"], scan_name="python_api_test") + await scan2._prep() await scan2.async_start_without_generator() scan_home = scan2.helpers.scans_dir / "python_api_test" out_file = scan_home / "output.json" @@ -25,6 +27,7 @@ async def test_python_api(): assert "python_api_test" in open(debug_log).read() scan3 = Scanner("127.0.0.1", output_modules=["json"], scan_name="scan_logging_test") + await scan3._prep() await scan3.async_start_without_generator() assert "scan_logging_test" not in open(scan_log).read() @@ -46,33 +49,37 @@ async def test_python_api(): assert os.environ["BBOT_TOOLS"] == str(Path(bbot_home) / "tools") # output modules override - scan4 = Scanner() - assert set(scan4.preset.output_modules) == {"csv", "json", "python", "txt"} - scan5 = Scanner(output_modules=["json"]) - assert set(scan5.preset.output_modules) == {"json"} + scan5 = Scanner() + assert set(scan5.preset.output_modules) == {"csv", "json", "python", "txt"} + scan6 = Scanner(output_modules=["json"]) + assert set(scan6.preset.output_modules) == {"json"} # custom target types custom_target_scan = Scanner("ORG:evilcorp") events = [e async for e in custom_target_scan.async_start()] - assert 1 == len([e for e in events if e.type == "ORG_STUB" and e.data == "evilcorp" and "target" in e.tags]) + + assert 1 == len([e for e in events if e.type == "ORG_STUB" and e.data == "evilcorp" and "seed" in e.tags]) # presets - scan6 = Scanner("evilcorp.com", presets=["subdomain-enum"]) - assert "sslcert" in scan6.preset.modules + scan7 = Scanner("evilcorp.com", presets=["subdomain-enum"]) + assert "sslcert" in scan7.preset.modules -def test_python_api_sync(): +@pytest.mark.asyncio +async def test_python_api_sync(clean_default_config): from bbot.scanner import Scanner # make sure events are properly yielded scan1 = Scanner("127.0.0.1") + await scan1._prep() events1 = [] - for event in scan1.start(): + async for event in scan1.async_start(): events1.append(event) assert any(e.type == "IP_ADDRESS" and e.data == "127.0.0.1" for e in events1) # make sure output files work scan2 = Scanner("127.0.0.1", output_modules=["json"], scan_name="python_api_test") - scan2.start_without_generator() + await scan2._prep() + await scan2.async_start_without_generator() out_file = scan2.helpers.scans_dir / "python_api_test" / "output.json" assert list(scan2.helpers.read_file(out_file)) # make sure config loads properly @@ -81,6 +88,27 @@ def test_python_api_sync(): assert os.environ["BBOT_TOOLS"] == str(Path(bbot_home) / "tools") +def test_python_api_sync_no_pending_tasks(): + """Test that no asyncio tasks remain pending after a sync scan completes. + + Regression test for https://github.com/blacklanternsecurity/bbot/issues/2508 + When using BBOT as a library (e.g. from Celery), orphaned asyncio tasks + would cause "Task was destroyed but it is pending!" warnings on shutdown. + """ + import asyncio + from bbot.scanner import Scanner + from bbot.core.helpers.async_helpers import get_event_loop + + scan = Scanner("127.0.0.1") + events = list(scan.start()) + assert any(e.type == "IP_ADDRESS" and e.data == "127.0.0.1" for e in events) + + # After the sync generator is exhausted, no tasks should remain pending + loop = get_event_loop() + pending = [t for t in asyncio.all_tasks(loop) if not t.done()] + assert len(pending) == 0, f"Found {len(pending)} pending tasks after scan: {pending}" + + def test_python_api_validation(): from bbot.scanner import Scanner, Preset @@ -95,23 +123,23 @@ def test_python_api_validation(): # invalid output module with pytest.raises(ValidationError) as error: Scanner(output_modules=["asdf"]) - assert str(error.value) == 'Could not find output module "asdf". Did you mean "teams"?' + assert str(error.value) == 'Could not find output module "asdf". Did you mean "nats"?' # invalid excluded module with pytest.raises(ValidationError) as error: Scanner(exclude_modules=["asdf"]) assert str(error.value) == 'Could not find module "asdf". Did you mean "asn"?' # invalid flag with pytest.raises(ValidationError) as error: - Scanner(flags=["asdf"]) - assert str(error.value) == 'Could not find flag "asdf". Did you mean "safe"?' + Scanner(flags=["activ"]) + assert str(error.value) == 'Could not find flag "activ". Did you mean "active"?' # invalid required flag with pytest.raises(ValidationError) as error: - Scanner(require_flags=["asdf"]) - assert str(error.value) == 'Could not find flag "asdf". Did you mean "safe"?' + Scanner(require_flags=["activ"]) + assert str(error.value) == 'Could not find flag "activ". Did you mean "active"?' # invalid excluded flag with pytest.raises(ValidationError) as error: - Scanner(exclude_flags=["asdf"]) - assert str(error.value) == 'Could not find flag "asdf". Did you mean "safe"?' + Scanner(exclude_flags=["activ"]) + assert str(error.value) == 'Could not find flag "activ". Did you mean "active"?' # output module as normal module with pytest.raises(ValidationError) as error: Scanner(modules=["json"]) @@ -119,7 +147,7 @@ def test_python_api_validation(): # normal module as output module with pytest.raises(ValidationError) as error: Scanner(output_modules=["robots"]) - assert str(error.value) == 'Could not find output module "robots". Did you mean "web_report"?' + assert str(error.value) == 'Could not find output module "robots". Did you mean "rabbitmq"?' # invalid preset type with pytest.raises(ValidationError) as error: Scanner(preset="asdf") diff --git a/bbot/test/test_step_1/test_regexes.py b/bbot/test/test_step_1/test_regexes.py index 94860fd4c0..d71f4c62db 100644 --- a/bbot/test/test_step_1/test_regexes.py +++ b/bbot/test/test_step_1/test_regexes.py @@ -351,9 +351,10 @@ def test_url_regexes(): @pytest.mark.asyncio async def test_regex_helper(): - from bbot import Scanner + from bbot.scanner import Scanner scan = Scanner("evilcorp.com", "evilcorp.org", "evilcorp.net", "evilcorp.co.uk") + await scan._prep() dns_name_regexes = regexes.event_type_regexes["DNS_NAME"] @@ -399,6 +400,7 @@ async def test_regex_helper(): # test yara hostname extractor helper scan = Scanner("evilcorp.com", "www.evilcorp.net", "evilcorp.co.uk") + await scan._prep() host_blob = """ https://evilcorp.com/ https://asdf.evilcorp.com/ @@ -424,5 +426,6 @@ async def test_regex_helper(): } scan = Scanner() + await scan._prep() extracted = await scan.extract_in_scope_hostnames(host_blob) assert extracted == set() diff --git a/bbot/test/test_step_1/test_scan.py b/bbot/test/test_step_1/test_scan.py index c5222d9591..d8233bf333 100644 --- a/bbot/test/test_step_1/test_scan.py +++ b/bbot/test/test_step_1/test_scan.py @@ -1,5 +1,3 @@ -from ipaddress import ip_network - from ..bbot_fixtures import * @@ -18,43 +16,44 @@ async def test_scan( blacklist=["1.1.1.1/28", "www.evilcorp.com"], modules=["ipneighbor"], ) - await scan0.load_modules() - assert scan0.whitelisted("1.1.1.1") - assert scan0.whitelisted("1.1.1.0") + await scan0._prep() + assert scan0.in_target("1.1.1.1") + assert scan0.in_target("1.1.1.0") assert scan0.blacklisted("1.1.1.15") assert not scan0.blacklisted("1.1.1.16") assert scan0.blacklisted("1.1.1.1/30") assert not scan0.blacklisted("1.1.1.1/27") assert not scan0.in_scope("1.1.1.1") - assert scan0.whitelisted("api.evilcorp.com") - assert scan0.whitelisted("www.evilcorp.com") + assert scan0.in_target("api.evilcorp.com") + assert scan0.in_target("www.evilcorp.com") assert not scan0.blacklisted("api.evilcorp.com") assert scan0.blacklisted("asdf.www.evilcorp.com") assert scan0.in_scope("test.api.evilcorp.com") assert not scan0.in_scope("test.www.evilcorp.com") assert not scan0.in_scope("www.evilcorp.co.uk") j = scan0.json - assert set(j["target"]["seeds"]) == {"1.1.1.0", "1.1.1.0/31", "evilcorp.com", "test.evilcorp.com"} - # we preserve the original whitelist inputs - assert set(j["target"]["whitelist"]) == {"1.1.1.0/32", "1.1.1.0/31", "evilcorp.com", "test.evilcorp.com"} - # but in the background they are collapsed - assert scan0.target.whitelist.hosts == {ip_network("1.1.1.0/31"), "evilcorp.com"} + assert not "seeds" in j["target"], "seeds should not be in target json" + # Positional arguments become the target + assert set(j["target"]["target"]) == {"1.1.1.0", "1.1.1.0/31", "evilcorp.com", "test.evilcorp.com"} + # Seeds are backfilled from target when not explicitly set + assert scan0.target.target.hosts == {"1.1.1.0/31", "evilcorp.com"} assert set(j["target"]["blacklist"]) == {"1.1.1.0/28", "www.evilcorp.com"} assert "ipneighbor" in j["preset"]["modules"] - scan1 = bbot_scanner("1.1.1.1", whitelist=["1.0.0.1"]) + scan1 = bbot_scanner("1.0.0.1", seeds=["1.1.1.1"]) assert not scan1.blacklisted("1.1.1.1") assert not scan1.blacklisted("1.0.0.1") - assert not scan1.whitelisted("1.1.1.1") - assert scan1.whitelisted("1.0.0.1") + assert not scan1.in_target("1.1.1.1") + assert scan1.in_target("1.0.0.1") assert scan1.in_scope("1.0.0.1") assert not scan1.in_scope("1.1.1.1") scan2 = bbot_scanner("1.1.1.1") + await scan2._prep() assert not scan2.blacklisted("1.1.1.1") assert not scan2.blacklisted("1.0.0.1") - assert scan2.whitelisted("1.1.1.1") - assert not scan2.whitelisted("1.0.0.1") + assert scan2.in_target("1.1.1.1") + assert not scan2.in_target("1.0.0.1") assert scan2.in_scope("1.1.1.1") assert not scan2.in_scope("1.0.0.1") @@ -65,29 +64,62 @@ async def test_scan( # make sure DNS resolution works scan4 = bbot_scanner("1.1.1.1", config={"dns": {"minimal": False}}) + await scan4._prep() await scan4.helpers.dns._mock_dns(dns_table) events = [] async for event in scan4.async_start(): events.append(event) - event_data = [e.data for e in events] + event_data = [e.pretty_string for e in events] assert "one.one.one.one" in event_data # make sure it doesn't work when you turn it off scan5 = bbot_scanner("1.1.1.1", config={"dns": {"minimal": True}}) + await scan5._prep() await scan5.helpers.dns._mock_dns(dns_table) events = [] async for event in scan5.async_start(): events.append(event) - event_data = [e.data for e in events] + event_data = [e.pretty_string for e in events] assert "one.one.one.one" not in event_data for scan in (scan0, scan1, scan2, scan4, scan5): await scan._cleanup() scan6 = bbot_scanner("a.foobar.io", "b.foobar.io", "c.foobar.io", "foobar.io") + await scan6._prep() assert len(scan6.dns_strings) == 1 +def test_seeds_target_separation(bbot_scanner): + """ + Test that when seeds are explicitly provided (via -s), they are properly separated from target. + """ + # Simulate: bbot -t 192.168.1.0/24 -s seed1.example.com seed2.example.com + scan = bbot_scanner( + "192.168.1.0/24", + seeds=["seed1.example.com", "seed2.example.com"], + ) + + # Verify target and seeds are properly separated in JSON + j = scan.json + assert set(j["target"]["target"]) == {"192.168.1.0/24"}, "Target should only contain the IP range" + assert set(j["target"]["seeds"]) == {"seed1.example.com", "seed2.example.com"}, ( + "Seeds should contain the DNS names, not the target" + ) + + # Verify target functionality + assert scan.in_target("192.168.1.1"), "IP in target range should be in target" + assert not scan.in_target("seed1.example.com"), "Seed DNS name should not be in target" + assert not scan.in_target("seed2.example.com"), "Seed DNS name should not be in target" + + # Verify seeds are accessible + assert "seed1.example.com" in scan.target.seeds.inputs, "seed1.example.com should be in seeds" + assert "seed2.example.com" in scan.target.seeds.inputs, "seed2.example.com should be in seeds" + assert "192.168.1.0/24" not in scan.target.seeds.inputs, ( + "Target should not be in seeds when seeds are explicitly provided" + ) + + @pytest.mark.asyncio async def test_task_scan_handle_event_timeout(bbot_scanner): from bbot.modules.base import BaseModule @@ -176,6 +208,96 @@ async def test_speed_counter(): assert 4 <= counter.speed <= 5 +@pytest.mark.asyncio +async def test_stats_attribution(): + from bbot.scanner.stats import ScanStats + from types import SimpleNamespace + + def mock_module(name, produced_events=None): + return SimpleNamespace(name=name, _stats_exclude=False, produced_events=produced_events or []) + + def mock_event(type, module, parent=None): + e = SimpleNamespace(type=type, module=module) + if parent is not None: + e.parent = parent + return e + + mock_scan = SimpleNamespace(status_frequency=60) + stats = ScanStats(mock_scan) + + httpx_mod = mock_module("httpx", ["URL", "HTTP_RESPONSE"]) + excavate_mod = mock_module("excavate", ["URL_UNVERIFIED", "WEB_PARAMETER"]) + ffuf_mod = mock_module("ffuf_shortnames", ["URL_UNVERIFIED"]) + ffuf2_mod = mock_module("ffuf", ["URL_UNVERIFIED"]) + speculate_mod = mock_module("speculate", ["DNS_NAME", "OPEN_TCP_PORT", "IP_ADDRESS", "FINDING", "ORG_STUB"]) + robots_mod = mock_module("robots", ["URL_UNVERIFIED"]) + + # 1) excavate discovers URL_UNVERIFIED from HTTP_RESPONSE, httpx verifies → excavate gets credit + for _ in range(5): + parent = mock_event("URL_UNVERIFIED", excavate_mod) + stats.event_produced(mock_event("URL", httpx_mod, parent=parent)) + + # 2) ffuf_shortnames discovers URL_UNVERIFIED, httpx verifies → ffuf_shortnames gets credit + for _ in range(3): + parent = mock_event("URL_UNVERIFIED", ffuf_mod) + stats.event_produced(mock_event("URL", httpx_mod, parent=parent)) + + # 3) ffuf discovers URL_UNVERIFIED, httpx verifies → ffuf gets credit + parent = mock_event("URL_UNVERIFIED", ffuf2_mod) + stats.event_produced(mock_event("URL", httpx_mod, parent=parent)) + + # 4) speculate (internal module) creates URL_UNVERIFIED, httpx verifies → httpx keeps credit + for _ in range(4): + parent = mock_event("URL_UNVERIFIED", speculate_mod) + stats.event_produced(mock_event("URL", httpx_mod, parent=parent)) + + # 5) robots discovers URL_UNVERIFIED, httpx verifies → robots gets credit + for _ in range(2): + parent = mock_event("URL_UNVERIFIED", robots_mod) + stats.event_produced(mock_event("URL", httpx_mod, parent=parent)) + + # 6) httpx discovers URL directly from OPEN_TCP_PORT (no URL_UNVERIFIED parent) → httpx keeps credit + for _ in range(2): + parent = mock_event("OPEN_TCP_PORT", mock_module("portscan")) + stats.event_produced(mock_event("URL", httpx_mod, parent=parent)) + + # 7) non-URL event types are unaffected + stats.event_produced(mock_event("DNS_NAME", mock_module("CNAME"))) + stats.event_produced(mock_event("STORAGE_BUCKET", mock_module("cloudcheck"))) + + # verify per-module produced counts + assert stats.module_stats["excavate"].produced == {"URL": 5} + assert stats.module_stats["ffuf_shortnames"].produced == {"URL": 3} + assert stats.module_stats["ffuf"].produced == {"URL": 1} + assert stats.module_stats["robots"].produced == {"URL": 2} + # httpx gets credit for speculate's 4 URLs + 2 from OPEN_TCP_PORT = 6 + assert stats.module_stats["httpx"].produced == {"URL": 6} + assert "speculate" not in stats.module_stats + assert stats.module_stats["CNAME"].produced == {"DNS_NAME": 1} + assert stats.module_stats["cloudcheck"].produced == {"STORAGE_BUCKET": 1} + + # verify the table output (sorted by produced_total descending) + table = stats.table() + header = table[0] + rows = table[1:] + assert header == ["Module", "Produced", "Consumed"] + + # build a dict of module_name -> produced_str from the table + table_dict = {row[0]: row[1] for row in rows} + assert table_dict["httpx"] == "6 (6 URL)" + assert table_dict["excavate"] == "5 (5 URL)" + assert table_dict["ffuf_shortnames"] == "3 (3 URL)" + assert table_dict["robots"] == "2 (2 URL)" + assert table_dict["ffuf"] == "1 (1 URL)" + assert table_dict["CNAME"] == "1 (1 DNS_NAME)" + assert table_dict["cloudcheck"] == "1 (1 STORAGE_BUCKET)" + assert "speculate" not in table_dict + + # verify sort order (highest produced first) + produced_totals = [stats.module_stats[row[0]].produced_total for row in rows] + assert produced_totals == sorted(produced_totals, reverse=True) + + @pytest.mark.asyncio async def test_python_output_matches_json(bbot_scanner): import json @@ -184,6 +306,7 @@ async def test_python_output_matches_json(bbot_scanner): "blacklanternsecurity.com", config={"speculate": True, "dns": {"minimal": False}, "scope": {"report_distance": 10}}, ) + await scan._prep() await scan.helpers.dns._mock_dns({"blacklanternsecurity.com": {"A": ["127.0.0.1"]}}) events = [e.json() async for e in scan.async_start()] output_json = scan.home / "output.json" @@ -194,7 +317,7 @@ async def test_python_output_matches_json(bbot_scanner): assert len(events) == 5 scan_events = [e for e in events if e["type"] == "SCAN"] assert len(scan_events) == 2 - assert all(isinstance(e["data"]["status"], str) for e in scan_events) + assert all(isinstance(e["data_json"]["status"], str) for e in scan_events) assert len([e for e in events if e["type"] == "DNS_NAME"]) == 1 assert len([e for e in events if e["type"] == "ORG_STUB"]) == 1 assert len([e for e in events if e["type"] == "IP_ADDRESS"]) == 1 @@ -220,10 +343,10 @@ async def test_huge_target_list(bbot_scanner, monkeypatch): @pytest.mark.asyncio -async def test_exclude_cdn(bbot_scanner, monkeypatch): +async def test_exclude_cdn(bbot_scanner, monkeypatch, clean_default_config): # test that CDN exclusion works - from bbot import Preset + from bbot.scanner import Preset dns_mock = { "evilcorp.com": {"A": ["127.0.0.1"]}, @@ -232,6 +355,7 @@ async def test_exclude_cdn(bbot_scanner, monkeypatch): # first, run a scan with no CDN exclusion scan = bbot_scanner("evilcorp.com") + await scan._prep() await scan.helpers._mock_dns(dns_mock) from bbot.modules.base import BaseModule @@ -241,14 +365,17 @@ class DummyModule(BaseModule): async def handle_event(self, event): if event.type == "DNS_NAME" and event.data == "evilcorp.com": - await self.emit_event("www.evilcorp.com", "DNS_NAME", parent=event, tags=["cdn-cloudflare"]) + await self.emit_event("www.evilcorp.com", "DNS_NAME", parent=event, tags=["cloudflare", "cdn"]) if event.type == "DNS_NAME" and event.data == "www.evilcorp.com": - await self.emit_event("www.evilcorp.com:80", "OPEN_TCP_PORT", parent=event, tags=["cdn-cloudflare"]) - await self.emit_event("www.evilcorp.com:443", "OPEN_TCP_PORT", parent=event, tags=["cdn-cloudflare"]) - await self.emit_event("www.evilcorp.com:8080", "OPEN_TCP_PORT", parent=event, tags=["cdn-cloudflare"]) + await self.emit_event("www.evilcorp.com:80", "OPEN_TCP_PORT", parent=event, tags=["cloudflare", "cdn"]) + await self.emit_event( + "www.evilcorp.com:443", "OPEN_TCP_PORT", parent=event, tags=["cloudflare", "cdn"] + ) + await self.emit_event( + "www.evilcorp.com:8080", "OPEN_TCP_PORT", parent=event, tags=["cloudflare", "cdn"] + ) dummy = DummyModule(scan=scan) - await scan._prep() scan.modules["dummy"] = dummy events = [e async for e in scan.async_start() if e.type in ("DNS_NAME", "OPEN_TCP_PORT")] assert set(e.data for e in events) == { @@ -264,11 +391,12 @@ async def handle_event(self, event): # then run a scan with --exclude-cdn enabled preset = Preset("evilcorp.com") preset.parse_args() - assert preset.bake().to_yaml() == "modules:\n- portfilter\n" + baked_preset = preset.bake() + assert baked_preset.to_yaml() == "modules:\n- portfilter\n" scan = bbot_scanner("evilcorp.com", preset=preset) + await scan._prep() await scan.helpers._mock_dns(dns_mock) dummy = DummyModule(scan=scan) - await scan._prep() scan.modules["dummy"] = dummy events = [e async for e in scan.async_start() if e.type in ("DNS_NAME", "OPEN_TCP_PORT")] assert set(e.data for e in events) == { @@ -279,7 +407,27 @@ async def handle_event(self, event): } +@pytest.mark.asyncio +async def test_scan_event_started_at_type(bbot_scanner): + """Regression test: started_at must be a float on both RUNNING and FINISHED SCAN events.""" + scan = bbot_scanner("127.0.0.1") + await scan._prep() + scan_events = [] + async for event in scan.async_start(): + if event.type == "SCAN": + scan_events.append(event) + + assert len(scan_events) == 2, f"Expected 2 SCAN events, got {len(scan_events)}" + for e in scan_events: + started_at = e.data.get("started_at") + status = e.data.get("status") + assert isinstance(started_at, float), ( + f"SCAN event (status={status}) started_at should be float, got {type(started_at).__name__}: {started_at!r}" + ) + + async def test_scan_name(bbot_scanner): scan = bbot_scanner("evilcorp.com", name="test_scan_name") + await scan._prep() assert scan.name == "test_scan_name" assert scan.preset.scan_name == "test_scan_name" diff --git a/bbot/test/test_step_1/test_scope.py b/bbot/test/test_step_1/test_scope.py index ac2d8c0426..d89c2df07b 100644 --- a/bbot/test/test_step_1/test_scope.py +++ b/bbot/test/test_step_1/test_scope.py @@ -5,6 +5,7 @@ class TestScopeBaseline(ModuleTestBase): targets = ["http://127.0.0.1:8888"] modules_overrides = ["httpx"] + config_overrides = {"omit_event_types": []} async def setup_after_prep(self, module_test): expect_args = {"method": "GET", "uri": "/"} @@ -21,7 +22,7 @@ def check(self, module_test, events): if e.type == "URL_UNVERIFIED" and str(e.host) == "127.0.0.1" and e.scope_distance == 0 - and "target" in e.tags + and "seed" in e.tags ] ) # we have two of these because the host module considers "always_emit" in its outgoing deduplication @@ -68,27 +69,46 @@ def check(self, module_test, events): assert not any(str(e.host) == "127.0.0.1" for e in events) -class TestScopeWhitelist(TestScopeBlacklist): - blacklist = [] - whitelist = ["255.255.255.255"] +class TestScopeCidrWithSeeds(ModuleTestBase): + """ + Test that when we have a CIDR as the target and DNS names as seeds, + only the DNS names that resolve to IPs within the CIDR should be detected as in-scope. + """ + + # Seeds: DNS names that will be tested + seeds = ["inscope.example.com", "outscope.example.com"] + # Target: CIDR that defines the scope + targets = ["192.168.1.0/24"] + modules_overrides = ["dnsresolve"] + + async def setup_after_prep(self, module_test): + # Mock DNS so that: + # - inscope.example.com resolves to 192.168.1.10 (inside the /24) + # - outscope.example.com resolves to 10.0.0.1 (outside the /24) + # This must be in setup_after_prep because the base fixture applies a default + # mock_dns after prep which replaces any earlier mocks. + await module_test.mock_dns( + { + "inscope.example.com": {"A": ["192.168.1.10"]}, + "outscope.example.com": {"A": ["10.0.0.1"]}, + } + ) def check(self, module_test, events): - assert len(events) == 4 - assert not any(e.type == "URL" for e in events) - assert 1 == len( - [ - e - for e in events - if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.scope_distance == 1 and "target" in e.tags - ] + # Find the DNS_NAME events for our seeds + inscope_events = [e for e in events if e.type == "DNS_NAME" and e.data == "inscope.example.com"] + outscope_events = [e for e in events if e.type == "DNS_NAME" and e.data == "outscope.example.com"] + + assert len(inscope_events) == 1, "inscope.example.com should be detected" + inscope_event = inscope_events[0] + assert inscope_event.scope_distance == 0, ( + f"inscope.example.com should be in-scope (scope_distance=0), got {inscope_event.scope_distance}" ) - assert 1 == len( - [ - e - for e in events - if e.type == "URL_UNVERIFIED" - and str(e.host) == "127.0.0.1" - and e.scope_distance == 1 - and "target" in e.tags - ] + assert "192.168.1.10" in inscope_event.resolved_hosts, "inscope.example.com should resolve to 192.168.1.10" + + assert len(outscope_events) > 0, "outscope.example.com should be detected" + outscope_event = outscope_events[0] + assert outscope_event.scope_distance > 0, ( + f"outscope.example.com should be out-of-scope (scope_distance>0), got {outscope_event.scope_distance}" ) + assert "10.0.0.1" in outscope_event.resolved_hosts, "outscope.example.com should resolve to 10.0.0.1" diff --git a/bbot/test/test_step_1/test_target.py b/bbot/test/test_step_1/test_target.py index a368718048..20977e8422 100644 --- a/bbot/test/test_step_1/test_target.py +++ b/bbot/test/test_step_1/test_target.py @@ -5,7 +5,7 @@ async def test_target_basic(bbot_scanner): from radixtarget import RadixTarget from ipaddress import ip_address, ip_network - from bbot.scanner.target import BBOTTarget, ScanSeeds + from bbot.scanner.target import BBOTTarget, ScanSeeds, ScanTarget scan1 = bbot_scanner("api.publicapis.org", "8.8.8.8/30", "2001:4860:4860::8888/126") scan2 = bbot_scanner("8.8.8.8/29", "publicapis.org", "2001:4860:4860::8888/125") @@ -13,8 +13,14 @@ async def test_target_basic(bbot_scanner): scan4 = bbot_scanner("8.8.8.8/29") scan5 = bbot_scanner() + await scan1._prep() + await scan2._prep() + await scan3._prep() + await scan4._prep() + await scan5._prep() + # test different types of inputs - target = BBOTTarget("evilcorp.com", "1.2.3.4/8") + target = BBOTTarget(target=["evilcorp.com", "1.2.3.4/8"]) assert "www.evilcorp.com" in target.seeds assert "www.evilcorp.com:80" in target.seeds assert "http://www.evilcorp.com:80" in target.seeds @@ -24,14 +30,13 @@ async def test_target_basic(bbot_scanner): assert ip_network("1.2.3.4/24", strict=False) in target.seeds event = scan1.make_event("https://www.evilcorp.com:80", dummy=True) assert event in target.seeds - with pytest.raises(ValueError): - ["asdf"] in target.seeds - with pytest.raises(ValueError): - target.seeds.get(["asdf"]) + assert ["asdf"] not in target.seeds + assert target.seeds.get(["asdf"]) is None assert not scan5.target.seeds - assert len(scan1.target.seeds) == 9 - assert len(scan4.target.seeds) == 8 + # radixtarget 4.x counts hosts/networks, not individual IPs + assert len(scan1.target.seeds) == 3 # api.publicapis.org, 8.8.8.8/30, 2001:4860:4860::8888/126 + assert len(scan4.target.seeds) == 1 # 8.8.8.8/29 assert "8.8.8.9" in scan1.target.seeds assert "8.8.8.12" not in scan1.target.seeds assert "8.8.8.8/31" in scan1.target.seeds @@ -54,37 +59,37 @@ async def test_target_basic(bbot_scanner): assert scan2.target.seeds == scan3.target.seeds assert scan4.target.seeds != scan1.target.seeds - assert not scan5.target.whitelist - assert len(scan1.target.whitelist) == 9 - assert len(scan4.target.whitelist) == 8 - assert "8.8.8.9" in scan1.target.whitelist - assert "8.8.8.12" not in scan1.target.whitelist - assert "8.8.8.8/31" in scan1.target.whitelist - assert "8.8.8.8/30" in scan1.target.whitelist - assert "8.8.8.8/29" not in scan1.target.whitelist - assert "2001:4860:4860::8889" in scan1.target.whitelist - assert "2001:4860:4860::888c" not in scan1.target.whitelist - assert "www.api.publicapis.org" in scan1.target.whitelist - assert "api.publicapis.org" in scan1.target.whitelist - assert "publicapis.org" not in scan1.target.whitelist - assert "bob@www.api.publicapis.org" in scan1.target.whitelist - assert "https://www.api.publicapis.org" in scan1.target.whitelist - assert "www.api.publicapis.org:80" in scan1.target.whitelist - assert scan1.make_event("https://[2001:4860:4860::8888]:80", dummy=True) in scan1.target.whitelist - assert scan1.make_event("[2001:4860:4860::8888]:80", "OPEN_TCP_PORT", dummy=True) in scan1.target.whitelist - assert scan1.make_event("[2001:4860:4860::888c]:80", "OPEN_TCP_PORT", dummy=True) not in scan1.target.whitelist - assert scan1.target.whitelist in scan2.target.whitelist - assert scan2.target.whitelist not in scan1.target.whitelist - assert scan3.target.whitelist in scan2.target.whitelist - assert scan2.target.whitelist == scan3.target.whitelist - assert scan4.target.whitelist != scan1.target.whitelist - - assert scan1.whitelisted("https://[2001:4860:4860::8888]:80") - assert scan1.whitelisted("[2001:4860:4860::8888]:80") - assert not scan1.whitelisted("[2001:4860:4860::888c]:80") - assert scan1.whitelisted("www.api.publicapis.org") - assert scan1.whitelisted("api.publicapis.org") - assert not scan1.whitelisted("publicapis.org") + assert not scan5.target.target + assert len(scan1.target.target) == 3 + assert len(scan4.target.target) == 1 + assert "8.8.8.9" in scan1.target.target + assert "8.8.8.12" not in scan1.target.target + assert "8.8.8.8/31" in scan1.target.target + assert "8.8.8.8/30" in scan1.target.target + assert "8.8.8.8/29" not in scan1.target.target + assert "2001:4860:4860::8889" in scan1.target.target + assert "2001:4860:4860::888c" not in scan1.target.target + assert "www.api.publicapis.org" in scan1.target.target + assert "api.publicapis.org" in scan1.target.target + assert "publicapis.org" not in scan1.target.target + assert "bob@www.api.publicapis.org" in scan1.target.target + assert "https://www.api.publicapis.org" in scan1.target.target + assert "www.api.publicapis.org:80" in scan1.target.target + assert scan1.make_event("https://[2001:4860:4860::8888]:80", dummy=True) in scan1.target.target + assert scan1.make_event("[2001:4860:4860::8888]:80", "OPEN_TCP_PORT", dummy=True) in scan1.target.target + assert scan1.make_event("[2001:4860:4860::888c]:80", "OPEN_TCP_PORT", dummy=True) not in scan1.target.target + assert scan1.target.target in scan2.target.target + assert scan2.target.target not in scan1.target.target + assert scan3.target.target in scan2.target.target + assert scan2.target.target == scan3.target.target + assert scan4.target.target != scan1.target.target + + assert scan1.in_target("https://[2001:4860:4860::8888]:80") + assert scan1.in_target("[2001:4860:4860::8888]:80") + assert not scan1.in_target("[2001:4860:4860::888c]:80") + assert scan1.in_target("www.api.publicapis.org") + assert scan1.in_target("api.publicapis.org") + assert not scan1.in_target("publicapis.org") assert scan1.target.seeds in scan2.target.seeds assert scan2.target.seeds not in scan1.target.seeds @@ -93,23 +98,23 @@ async def test_target_basic(bbot_scanner): assert scan4.target.seeds != scan1.target.seeds assert str(scan1.target.seeds.get("8.8.8.9").host) == "8.8.8.8/30" - assert str(scan1.target.whitelist.get("8.8.8.9").host) == "8.8.8.8/30" + assert str(scan1.target.target.get("8.8.8.9").host) == "8.8.8.8/30" assert scan1.target.seeds.get("8.8.8.12") is None - assert scan1.target.whitelist.get("8.8.8.12") is None + assert scan1.target.target.get("8.8.8.12") is None assert str(scan1.target.seeds.get("2001:4860:4860::8889").host) == "2001:4860:4860::8888/126" - assert str(scan1.target.whitelist.get("2001:4860:4860::8889").host) == "2001:4860:4860::8888/126" + assert str(scan1.target.target.get("2001:4860:4860::8889").host) == "2001:4860:4860::8888/126" assert scan1.target.seeds.get("2001:4860:4860::888c") is None - assert scan1.target.whitelist.get("2001:4860:4860::888c") is None + assert scan1.target.target.get("2001:4860:4860::888c") is None assert str(scan1.target.seeds.get("www.api.publicapis.org").host) == "api.publicapis.org" - assert str(scan1.target.whitelist.get("www.api.publicapis.org").host) == "api.publicapis.org" + assert str(scan1.target.target.get("www.api.publicapis.org").host) == "api.publicapis.org" assert scan1.target.seeds.get("publicapis.org") is None - assert scan1.target.whitelist.get("publicapis.org") is None + assert scan1.target.target.get("publicapis.org") is None target = RadixTarget("evilcorp.com") assert "com" not in target assert "evilcorp.com" in target assert "www.evilcorp.com" in target - strict_target = RadixTarget("evilcorp.com", strict_dns_scope=True) + strict_target = RadixTarget("evilcorp.com", strict_scope=True) assert "com" not in strict_target assert "evilcorp.com" in strict_target assert "www.evilcorp.com" not in strict_target @@ -119,7 +124,7 @@ async def test_target_basic(bbot_scanner): assert "com" not in target assert "evilcorp.com" in target assert "www.evilcorp.com" in target - strict_target = RadixTarget(strict_dns_scope=True) + strict_target = RadixTarget(strict_scope=True) strict_target.add("evilcorp.com") assert "com" not in strict_target assert "evilcorp.com" in strict_target @@ -128,58 +133,57 @@ async def test_target_basic(bbot_scanner): # test target hashing target1 = BBOTTarget() - target1.whitelist.add("evilcorp.com") - target1.whitelist.add("1.2.3.4/24") - target1.whitelist.add("https://evilcorp.net:8080") + target1.target.add("evilcorp.com") + target1.target.add("1.2.3.4/24") + target1.target.add("https://evilcorp.net:8080") target1.seeds.add("evilcorp.com") target1.seeds.add("1.2.3.4/24") target1.seeds.add("https://evilcorp.net:8080") target2 = BBOTTarget() - target2.whitelist.add("bob@evilcorp.org") - target2.whitelist.add("evilcorp.com") - target2.whitelist.add("1.2.3.4/24") - target2.whitelist.add("https://evilcorp.net:8080") + target2.target.add("bob@evilcorp.org") + target2.target.add("evilcorp.com") + target2.target.add("1.2.3.4/24") + target2.target.add("https://evilcorp.net:8080") target2.seeds.add("bob@evilcorp.org") target2.seeds.add("evilcorp.com") target2.seeds.add("1.2.3.4/24") target2.seeds.add("https://evilcorp.net:8080") - # make sure it's a sha1 hash assert isinstance(target1.hash, bytes) - assert len(target1.hash) == 20 + assert len(target1.hash) == 24 # hashes shouldn't match yet assert target1.hash != target2.hash assert target1.scope_hash != target2.scope_hash # add missing email - target1.whitelist.add("bob@evilcorp.org") + target1.target.add("bob@evilcorp.org") assert target1.hash != target2.hash assert target1.scope_hash == target2.scope_hash target1.seeds.add("bob@evilcorp.org") # now they should match assert target1.hash == target2.hash - # test default whitelist - bbottarget = BBOTTarget("http://1.2.3.4:8443", "bob@evilcorp.com") - assert bbottarget.seeds.hosts == {ip_network("1.2.3.4"), "evilcorp.com"} - assert bbottarget.whitelist.hosts == {ip_network("1.2.3.4"), "evilcorp.com"} + # test default target + bbottarget = BBOTTarget(target=["http://1.2.3.4:8443", "bob@evilcorp.com"]) + + assert bbottarget.seeds.hosts == {"1.2.3.4/32", "evilcorp.com"} + assert bbottarget.target.hosts == {"1.2.3.4/32", "evilcorp.com"} assert {e.data for e in bbottarget.seeds.event_seeds} == {"http://1.2.3.4:8443/", "bob@evilcorp.com"} - assert {e.data for e in bbottarget.whitelist.event_seeds} == {"1.2.3.4/32", "evilcorp.com"} + assert {e.data for e in bbottarget.target.event_seeds} == {"http://1.2.3.4:8443/", "bob@evilcorp.com"} - bbottarget1 = BBOTTarget("evilcorp.com", "evilcorp.net", whitelist=["1.2.3.4/24"], blacklist=["1.2.3.4"]) - bbottarget2 = BBOTTarget("evilcorp.com", "evilcorp.net", whitelist=["1.2.3.0/24"], blacklist=["1.2.3.4"]) - bbottarget3 = BBOTTarget("evilcorp.com", whitelist=["1.2.3.4/24"], blacklist=["1.2.3.4"]) - bbottarget5 = BBOTTarget("evilcorp.com", "evilcorp.net", whitelist=["1.2.3.0/24"], blacklist=["1.2.3.4"]) + bbottarget1 = BBOTTarget(seeds=["evilcorp.com", "evilcorp.net"], target=["1.2.3.4/24"], blacklist=["1.2.3.4"]) + bbottarget2 = BBOTTarget(seeds=["evilcorp.com", "evilcorp.net"], target=["1.2.3.0/24"], blacklist=["1.2.3.4"]) + bbottarget3 = BBOTTarget(seeds=["evilcorp.com"], target=["1.2.3.4/24"], blacklist=["1.2.3.4"]) + bbottarget5 = BBOTTarget(seeds=["evilcorp.com", "evilcorp.net"], target=["1.2.3.0/24"], blacklist=["1.2.3.4"]) bbottarget6 = BBOTTarget( - "evilcorp.com", "evilcorp.net", whitelist=["1.2.3.0/24"], blacklist=["1.2.3.4"], strict_scope=True + seeds=["evilcorp.com", "evilcorp.net"], target=["1.2.3.0/24"], blacklist=["1.2.3.4"], strict_scope=True ) - bbottarget8 = BBOTTarget("1.2.3.0/24", whitelist=["evilcorp.com", "evilcorp.net"], blacklist=["1.2.3.4"]) - bbottarget9 = BBOTTarget("evilcorp.com", "evilcorp.net", whitelist=["1.2.3.0/24"], blacklist=["1.2.3.4"]) + bbottarget8 = BBOTTarget(seeds=["1.2.3.0/24"], target=["evilcorp.com", "evilcorp.net"], blacklist=["1.2.3.4"]) + bbottarget9 = BBOTTarget(seeds=["evilcorp.com", "evilcorp.net"], target=["1.2.3.0/24"], blacklist=["1.2.3.4"]) - # make sure it's a sha1 hash assert isinstance(bbottarget1.hash, bytes) - assert len(bbottarget1.hash) == 20 + assert len(bbottarget1.hash) == 24 assert bbottarget1 == bbottarget2 assert bbottarget2 == bbottarget1 @@ -191,9 +195,9 @@ async def test_target_basic(bbot_scanner): assert bbottarget1 == bbottarget3 assert bbottarget3 == bbottarget1 - # adding different events (but with same host) to whitelist should not change hash (since only hosts matter) - bbottarget1.whitelist.add("http://evilcorp.co.nz") - bbottarget2.whitelist.add("evilcorp.co.nz") + # adding different events (but with same host) to target should not change hash (since only hosts matter) + bbottarget1.target.add("http://evilcorp.co.nz") + bbottarget2.target.add("evilcorp.co.nz") assert bbottarget1 == bbottarget2 assert bbottarget2 == bbottarget1 @@ -207,28 +211,28 @@ async def test_target_basic(bbot_scanner): assert bbottarget5 != bbottarget6 assert bbottarget6 != bbottarget5 - # make sure swapped target <--> whitelist result in different hash + # make sure swapped target <--> blacklist result in different hash assert bbottarget8 != bbottarget9 assert bbottarget9 != bbottarget8 # make sure duplicate events don't change hash - target1 = BBOTTarget("https://evilcorp.com") - target2 = BBOTTarget("https://evilcorp.com") + target1 = BBOTTarget(target=["https://evilcorp.com"]) + target2 = BBOTTarget(target=["https://evilcorp.com"]) assert target1 == target2 target1.seeds.add("https://evilcorp.com:443") assert target1 == target2 - # make sure hosts are collapsed in whitelist and blacklist + # make sure hosts are collapsed in target and blacklist bbottarget = BBOTTarget( - "http://evilcorp.com:8080", - whitelist=["evilcorp.net:443", "http://evilcorp.net:8080"], + seeds=["http://evilcorp.com:8080"], + target=["evilcorp.net:443", "http://evilcorp.net:8080"], blacklist=["http://evilcorp.org:8080", "evilcorp.org:443"], ) # base class is not iterable with pytest.raises(TypeError): assert list(bbottarget) == ["http://evilcorp.com:8080/"] assert {e.data for e in bbottarget.seeds} == {"http://evilcorp.com:8080/"} - assert {e.data for e in bbottarget.whitelist} == {"evilcorp.net:443", "http://evilcorp.net:8080/"} + assert {e.data for e in bbottarget.target} == {"evilcorp.net:443", "http://evilcorp.net:8080/"} assert {e.data for e in bbottarget.blacklist} == {"http://evilcorp.org:8080/", "evilcorp.org:443"} # test org stub as target @@ -247,6 +251,7 @@ async def test_target_basic(bbot_scanner): # users + orgs + domains scan = bbot_scanner("USER:evilcorp", "ORG:evilcorp", "evilcorp.com") + await scan._prep() await scan.helpers.dns._mock_dns( { "evilcorp.com": {"A": ["1.2.3.4"]}, @@ -258,10 +263,8 @@ async def test_target_basic(bbot_scanner): # verify hash values bbottarget = BBOTTarget( - "1.2.3.0/24", - "http://www.evilcorp.net", - "bob@fdsa.evilcorp.net", - whitelist=["evilcorp.com", "bob@www.evilcorp.com", "evilcorp.net"], + seeds=["1.2.3.0/24", "http://www.evilcorp.net", "bob@fdsa.evilcorp.net"], + target=["evilcorp.com", "bob@www.evilcorp.com", "evilcorp.net"], blacklist=["1.2.3.4", "4.3.2.1/24", "http://1.2.3.4", "bob@asdf.evilcorp.net"], ) assert {e.data for e in bbottarget.seeds.event_seeds} == { @@ -269,7 +272,7 @@ async def test_target_basic(bbot_scanner): "http://www.evilcorp.net/", "bob@fdsa.evilcorp.net", } - assert {e.data for e in bbottarget.whitelist.event_seeds} == { + assert {e.data for e in bbottarget.target.event_seeds} == { "evilcorp.com", "evilcorp.net", "bob@www.evilcorp.com", @@ -280,20 +283,20 @@ async def test_target_basic(bbot_scanner): "http://1.2.3.4/", "bob@asdf.evilcorp.net", } - assert set(bbottarget.seeds.hosts) == {ip_network("1.2.3.0/24"), "www.evilcorp.net", "fdsa.evilcorp.net"} - assert set(bbottarget.whitelist.hosts) == {"evilcorp.com", "evilcorp.net"} - assert set(bbottarget.blacklist.hosts) == {ip_network("1.2.3.4/32"), ip_network("4.3.2.0/24"), "asdf.evilcorp.net"} - assert bbottarget.hash == b"\xb3iU\xa8#\x8aq\x84/\xc5\xf2;\x11\x11\x0c&\xea\x07\xd4Q" - assert bbottarget.scope_hash == b"f\xe1\x01c^3\xf5\xd24B\x87P\xa0Glq0p3J" - assert bbottarget.seeds.hash == b"V\n\xf5\x1d\x1f=i\xbc\\\x15o\xc2p\xb2\x84\x97\xfeR\xde\xc1" - assert bbottarget.whitelist.hash == b"\x8e\xd0\xa76\x8em4c\x0e\x1c\xfdA\x9d*sv}\xeb\xc4\xc4" - assert bbottarget.blacklist.hash == b'\xf7\xaf\xa1\xda4"C:\x13\xf42\xc3,\xc3\xa9\x9f\x15\x15n\\' + assert bbottarget.seeds.hosts == {"1.2.3.0/24", "www.evilcorp.net", "fdsa.evilcorp.net"} + assert set(bbottarget.target.hosts) == {"evilcorp.com", "evilcorp.net"} + assert bbottarget.blacklist.hosts == {"1.2.3.4/32", "4.3.2.0/24", "asdf.evilcorp.net"} + assert bbottarget.hash == b"W\x1ai\x9f\xd6\x13\x87\xdd\x9cNP\xcf\xca4[6F\xc0U\x13\xfbd\xd9\xf3" + assert bbottarget.scope_hash == b"\x9cNP\xcf\xca4[6F\xc0U\x13\xfbd\xd9\xf3" + assert bbottarget.seeds.hash == b"W\x1ai\x9f\xd6\x13\x87\xdd" + assert bbottarget.target.hash == b"\x9cNP\xcf\xca4[6" + assert bbottarget.blacklist.hash == b"F\xc0U\x13\xfbd\xd9\xf3" scan = bbot_scanner( - "http://www.evilcorp.net", - "1.2.3.0/24", - "bob@fdsa.evilcorp.net", - whitelist=["evilcorp.net", "evilcorp.com", "bob@www.evilcorp.com"], + "evilcorp.net", + "evilcorp.com", + "bob@www.evilcorp.com", + seeds=["http://www.evilcorp.net", "1.2.3.0/24", "bob@fdsa.evilcorp.net"], blacklist=["bob@asdf.evilcorp.net", "1.2.3.4", "4.3.2.1/24", "http://1.2.3.4"], ) events = [e async for e in scan.async_start()] @@ -302,24 +305,24 @@ async def test_target_basic(bbot_scanner): target_dict = scan_events[0].data["target"] assert target_dict["seeds"] == ["1.2.3.0/24", "bob@fdsa.evilcorp.net", "http://www.evilcorp.net/"] - assert target_dict["whitelist"] == ["bob@www.evilcorp.com", "evilcorp.com", "evilcorp.net"] + assert target_dict["target"] == ["bob@www.evilcorp.com", "evilcorp.com", "evilcorp.net"] assert target_dict["blacklist"] == ["1.2.3.4", "4.3.2.0/24", "bob@asdf.evilcorp.net", "http://1.2.3.4/"] assert target_dict["strict_scope"] is False - assert target_dict["hash"] == "b36955a8238a71842fc5f23b11110c26ea07d451" - assert target_dict["seed_hash"] == "560af51d1f3d69bc5c156fc270b28497fe52dec1" - assert target_dict["whitelist_hash"] == "8ed0a7368e6d34630e1cfd419d2a73767debc4c4" - assert target_dict["blacklist_hash"] == "f7afa1da3422433a13f432c32cc3a99f15156e5c" - assert target_dict["scope_hash"] == "66e101635e33f5d234428750a0476c713070334a" + assert target_dict["hash"] == "571a699fd61387dd9c4e50cfca345b3646c05513fb64d9f3" + assert target_dict["seed_hash"] == "571a699fd61387dd" + assert target_dict["target_hash"] == "9c4e50cfca345b36" + assert target_dict["blacklist_hash"] == "46c05513fb64d9f3" + assert target_dict["scope_hash"] == "9c4e50cfca345b3646c05513fb64d9f3" - # make sure child subnets/IPs don't get added to whitelist/blacklist + # make sure child subnets/IPs don't get added to target/blacklist target = RadixTarget("1.2.3.4/24", "1.2.3.4/28", acl_mode=True) - assert set(target) == {ip_network("1.2.3.0/24")} + assert set(target) == {"1.2.3.0/24"} target = RadixTarget("1.2.3.4/28", "1.2.3.4/24", acl_mode=True) - assert set(target) == {ip_network("1.2.3.0/24")} + assert set(target) == {"1.2.3.0/24"} target = RadixTarget("1.2.3.4/28", "1.2.3.4", acl_mode=True) - assert set(target) == {ip_network("1.2.3.0/28")} + assert set(target) == {"1.2.3.0/28"} target = RadixTarget("1.2.3.4", "1.2.3.4/28", acl_mode=True) - assert set(target) == {ip_network("1.2.3.0/28")} + assert set(target) == {"1.2.3.0/28"} # same but for domains target = RadixTarget("evilcorp.com", "www.evilcorp.com", acl_mode=True) @@ -328,8 +331,10 @@ async def test_target_basic(bbot_scanner): assert set(target) == {"evilcorp.com"} # make sure strict_scope doesn't mess us up - target = RadixTarget("evilcorp.co.uk", "www.evilcorp.co.uk", acl_mode=True, strict_dns_scope=True) - assert set(target.hosts) == {"evilcorp.co.uk", "www.evilcorp.co.uk"} + # radixtarget 4.x: strict_scope and acl_mode are mutually exclusive, + # so ACLTarget skips acl_mode when strict_scope is True + target = ScanTarget("evilcorp.co.uk", "www.evilcorp.co.uk", strict_scope=True) + assert target.hosts == {"evilcorp.co.uk", "www.evilcorp.co.uk"} assert "evilcorp.co.uk" in target assert "www.evilcorp.co.uk" in target assert "api.evilcorp.co.uk" not in target @@ -346,6 +351,237 @@ async def test_target_basic(bbot_scanner): assert {e.data for e in events} == {"http://evilcorp.com/", "evilcorp.com:443"} +@pytest.mark.asyncio +async def test_asn_targets(bbot_scanner): + """Test ASN target parsing, validation, and functionality.""" + from bbot.core.event.helpers import EventSeed + from bbot.scanner.target import BBOTTarget + + # Test ASN target parsing with different formats + for asn_format in ("ASN:15169", "AS:15169", "AS15169", "asn:15169", "as:15169", "as15169"): + event_seed = EventSeed(asn_format) + assert event_seed.type == "ASN" + assert event_seed.data == "15169" + assert event_seed.input == "ASN:15169" + + # Test ASN targets in BBOTTarget (target= is the primary input; seeds auto-populate from target) + target = BBOTTarget(target=["ASN:15169"]) + assert "ASN:15169" in target.seeds.inputs + + # Test ASN with other targets + target = BBOTTarget(target=["ASN:15169", "evilcorp.com", "1.2.3.4/24"]) + assert "ASN:15169" in target.seeds.inputs + assert "evilcorp.com" in target.seeds.inputs + assert "1.2.3.0/24" in target.seeds.inputs # IP ranges are normalized to network address + + # Test ASN targets must be expanded before being useful in scope/blacklist + # Direct ASN targets don't work since they have no host + # Instead, test that the ASN input is captured correctly + target = BBOTTarget(target=["evilcorp.com"]) + # ASN targets should be added to seeds + target.seeds.add("ASN:15169") + assert "ASN:15169" in target.seeds.inputs + + # Test ASN target expansion with real asndb (Google's AS15169) + target = BBOTTarget(target=["ASN:15169"]) + + # Verify initial state + initial_hosts = len(target.seeds.hosts) + initial_seeds = len(target.seeds.event_seeds) + + # Generate children (expand ASN to IP ranges) + await target.generate_children() + + # After expansion, should have additional IP range seeds + assert len(target.seeds.event_seeds) > initial_seeds + assert len(target.seeds.hosts) > initial_hosts + + # Google's AS15169 owns 8.8.8.0/24 + assert "8.8.8.0/24" in target.seeds.hosts + assert "8.8.8.0/24" in target.target.hosts + + +@pytest.mark.asyncio +async def test_asn_targets_integration(bbot_scanner): + """Test ASN targets with full scanner integration.""" + from unittest.mock import AsyncMock, MagicMock, patch + + mock_client = MagicMock() + mock_client.lookup_asn = AsyncMock( + return_value={ + "asn": 15169, + "asn_name": "GOOGLE", + "org": "Google LLC", + "country": "US", + "subnets": ["8.8.8.0/24", "8.8.4.0/24"], + } + ) + mock_client.cleanup = AsyncMock() + + # Create scanner with ASN target + scan = bbot_scanner("ASN:15169") + + with patch("asndb.ASNDB", return_value=mock_client): + # Initialize scan to access preset and target + await scan._prep() + + # Verify target was parsed correctly + assert "ASN:15169" in scan.preset.target.seeds.inputs + + # Verify expansion worked (generate_children is called during _prep) + + assert "8.8.8.0/24" in scan.preset.target.seeds.hosts + assert "8.8.4.0/24" in scan.preset.target.seeds.hosts + + # Test scope checking with expanded ranges + assert scan.in_scope("8.8.8.1") + assert scan.in_scope("8.8.4.1") + assert not scan.in_scope("1.1.1.1") + + +@pytest.mark.asyncio +async def test_asn_targets_edge_cases(bbot_scanner): + """Test edge cases and error handling for ASN targets.""" + from bbot.core.event.helpers import EventSeed + from bbot.errors import ValidationError + from bbot.scanner.target import BBOTTarget + + # Test invalid ASN formats that should raise ValidationError + invalid_formats_validation_error = ["ASN:", "AS:", "ASN:abc", "AS:xyz", "ASN:-1"] + for invalid_format in invalid_formats_validation_error: + with pytest.raises(ValidationError): + EventSeed(invalid_format) + + # Test invalid ASN format that gets parsed as something else + event_seed = EventSeed("ASNXYZ") + assert event_seed.type == "DNS_NAME" # Falls back to DNS parsing + assert event_seed.data == "asnxyz" + + # Test valid edge cases + valid_formats = ["ASN:0", "AS:0", "ASN:4294967295", "AS:4294967295"] + for valid_format in valid_formats[:2]: # Test just a couple to avoid huge ASN numbers + event_seed = EventSeed(valid_format) + assert event_seed.type == "ASN" + + # Test ASN with no subnets + from unittest.mock import AsyncMock, MagicMock, patch + + mock_empty_client = MagicMock() + mock_empty_client.lookup_asn = AsyncMock(return_value=None) + mock_empty_client.cleanup = AsyncMock() + + target = BBOTTarget(target=["ASN:99999"]) # Non-existent ASN + + initial_seeds = len(target.seeds.event_seeds) + with patch("asndb.ASNDB", return_value=mock_empty_client): + await target.generate_children() + + # Should not add any new seeds for empty ASN + assert len(target.seeds.event_seeds) == initial_seeds + + # Test that ASN blacklisting would happen after expansion + # Since ASN targets can't be directly added to blacklist (no host), + # the proper way would be to expand the ASN and then blacklist the IP ranges + target = BBOTTarget(target=["evilcorp.com"]) + # This demonstrates the intended usage pattern - add expanded IP ranges to blacklist + target.blacklist.add("8.8.8.0/24") # Would come from ASN expansion + assert "8.8.8.0/24" in target.blacklist.inputs + + +@pytest.mark.asyncio +async def test_asn_blacklist_functionality(bbot_scanner): + """Test ASN blacklisting: IP range target with ASN in blacklist should expand and block subnets.""" + from unittest.mock import AsyncMock, MagicMock, patch + + mock_client = MagicMock() + mock_client.lookup_asn = AsyncMock( + return_value={ + "asn": 15169, + "subnets": ["8.8.8.0/24"], + } + ) + mock_client.cleanup = AsyncMock() + + with patch("asndb.ASNDB", return_value=mock_client): + # Target: 8.8.8.0/23 (includes 8.8.8.0/24 and 8.8.9.0/24) + # Blacklist: ASN:15169 (should expand to 8.8.8.0/24 and block it) + scan = bbot_scanner("8.8.8.0/23", blacklist=["ASN:15169"]) + await scan._prep() + + # The ASN should have been expanded and the subnet should be in blacklist + assert "8.8.8.0/24" in scan.preset.target.blacklist.hosts + + # 8.8.8.x should be blocked (ASN subnet in blacklist) + assert not scan.in_scope("8.8.8.1") + assert not scan.in_scope("8.8.8.8") + assert not scan.in_scope("8.8.8.255") + + # 8.8.9.x should be allowed (in target but ASN doesn't cover this) + assert scan.in_scope("8.8.9.1") + assert scan.in_scope("8.8.9.8") + assert scan.in_scope("8.8.9.255") + + # IPs outside the target should not be in scope + assert not scan.in_scope("8.8.7.1") + assert not scan.in_scope("8.8.10.1") + + +@pytest.mark.asyncio +async def test_asn_len_overflow(bbot_scanner): + """Regression test: len() on targets with many ASN subnets must not overflow. + + RadixTarget.__len__() counts individual IPs, which can exceed sys.maxsize + for large ASNs (e.g. AS15169 with 1000+ subnets). The scanner log message + must use len(event_seeds) instead. + """ + from unittest.mock import AsyncMock, MagicMock, patch + + # Simulate a large ASN with many /16 subnets — total IPs would overflow an index + many_subnets = [f"10.{i}.0.0/16" for i in range(200)] + + mock_client = MagicMock() + mock_client.lookup_asn = AsyncMock(return_value={"asn": 99999, "subnets": many_subnets}) + mock_client.cleanup = AsyncMock() + + with patch("asndb.ASNDB", return_value=mock_client): + scan = bbot_scanner("ASN:99999") + # _prep() calls generate_children() and then does len(self.seeds.event_seeds) + # Before the fix, this raised OverflowError from len() on the RadixTarget + await scan._prep() + + # Verify expansion worked + assert len(scan.preset.target.seeds.event_seeds) > 200 + + +@pytest.mark.asyncio +async def test_asn_event_json_serialization(bbot_scanner): + """Regression test: ASN events must serialize and deserialize correctly.""" + from unittest.mock import AsyncMock, MagicMock, patch + from bbot.core.event.base import event_from_json + + mock_client = MagicMock() + mock_client.lookup_asn = AsyncMock(return_value={"asn": 12345, "subnets": ["192.0.2.0/24"]}) + mock_client.cleanup = AsyncMock() + + with patch("asndb.ASNDB", return_value=mock_client): + scan = bbot_scanner("ASN:12345") + await scan._prep() + + # Create an ASN event like the scanner does (bare int input) + asn_event = scan.make_event(12345, "ASN", parent=scan.root_event) + assert asn_event.data == {"asn": 12345} + + # Serialize to JSON + j = asn_event.json() + assert j["type"] == "ASN" + assert j["data_json"] == {"asn": 12345} + + # Round-trip: reconstruct from JSON + reconstructed = event_from_json(j) + assert reconstructed.type == "ASN" + assert reconstructed.data == {"asn": 12345} + + @pytest.mark.asyncio async def test_blacklist_regex(bbot_scanner, bbot_httpserver): from bbot.scanner.target import ScanBlacklist @@ -390,9 +626,10 @@ async def test_blacklist_regex(bbot_scanner, bbot_httpserver): # make sure URL is detected normally scan = bbot_scanner("http://127.0.0.1:8888/", presets=["spider"], config={"excavate": True}, debug=True) + await scan._prep() assert {r.pattern for r in scan.target.blacklist.blacklist_regexes} == {r"/.*(sign|log)[_-]?out"} events = [e async for e in scan.async_start()] - urls = [e.data for e in events if e.type == "URL"] + urls = [e.url for e in events if e.type == "URL"] assert len(urls) == 2 assert set(urls) == {"http://127.0.0.1:8888/", "http://127.0.0.1:8888/asdfevil333asdf"} @@ -404,6 +641,7 @@ async def test_blacklist_regex(bbot_scanner, bbot_httpserver): config={"excavate": True}, debug=True, ) + await scan._prep() assert len(scan.target.blacklist) == 2 assert scan.target.blacklist.blacklist_regexes assert {r.pattern for r in scan.target.blacklist.blacklist_regexes} == { @@ -411,6 +649,187 @@ async def test_blacklist_regex(bbot_scanner, bbot_httpserver): r"/.*(sign|log)[_-]?out", } events = [e async for e in scan.async_start()] - urls = [e.data for e in events if e.type == "URL"] + urls = [e.url for e in events if e.type == "URL"] assert len(urls) == 1 assert set(urls) == {"http://127.0.0.1:8888/"} + + +def test_no_double_parsing(): + """Regression test: when seeds are auto-populated from target, EventSeed parsing + should happen only once (via ScanTarget), not twice. BBOTTarget should pass + pre-parsed EventSeed objects to ScanSeeds instead of raw strings.""" + from unittest.mock import patch + from bbot.scanner.target import BBOTTarget + from bbot.core.event.helpers import EventSeed as _real_EventSeed + + targets = ["evilcorp.com", "1.2.3.4", "https://example.com", "10.0.0.0/24"] + + call_count = 0 + original_EventSeed = _real_EventSeed + + def counting_EventSeed(input): + nonlocal call_count + call_count += 1 + return original_EventSeed(input) + + with patch("bbot.scanner.target.EventSeed", side_effect=counting_EventSeed): + BBOTTarget(target=targets) + + # EventSeed should be called once per target (for ScanTarget), not twice + assert call_count == len(targets), ( + f"EventSeed was called {call_count} times for {len(targets)} targets; " + f"expected {len(targets)} (seeds should reuse pre-parsed EventSeed objects)" + ) + + +def test_target_pickle(): + """BBOTTarget must survive pickle round-trips (used by HTTP engine subprocess).""" + import pickle + + from bbot.scanner.target import BBOTTarget + + target = BBOTTarget( + target=["evilcorp.com", "1.2.3.0/24"], + blacklist=["bad.evilcorp.com"], + strict_scope=False, + ) + + data = pickle.dumps(target) + restored = pickle.loads(data) + + # scope checks work after unpickling + assert restored.in_target("evilcorp.com") + assert restored.in_target("www.evilcorp.com") + assert restored.in_target("1.2.3.4") + assert not restored.in_target("9.9.9.9") + + # blacklist works after unpickling + assert restored.blacklisted("bad.evilcorp.com") + assert not restored.in_scope("bad.evilcorp.com") + assert restored.in_scope("good.evilcorp.com") + + # hashes match + assert target.hash == restored.hash + + +def test_target_comments(): + """Target strings support # comments — both full-line and inline.""" + from bbot.scanner.target import BBOTTarget + + target = BBOTTarget( + target=[ + "# this is a full-line comment", + "evilcorp.com # main evilcorp domain", + " # indented comment ", + "1.2.3.0/24 # internal network", + "othercorp.com", + ], + ) + + # comment-only lines are ignored + assert len(target.target) == 3 + + # inline comments are stripped — targets work normally + assert target.in_target("evilcorp.com") + assert target.in_target("www.evilcorp.com") + assert target.in_target("1.2.3.4") + assert target.in_target("othercorp.com") + + # the comment text itself is not a target + assert not target.in_target("main") + assert not target.in_target("internal") + + +def test_target_comments_url_fragment_not_stripped(): + """A # inside a URL (fragment) must NOT be treated as a comment. + + BBOT's URL normalisation may drop fragments, but the important thing + is that the host is still recognised as a valid target. + """ + from bbot.scanner.target import BBOTTarget + + target = BBOTTarget(target=["http://evilcorp.com/page#section"]) + assert target.in_target("evilcorp.com") + assert len(target.target) == 1 + + +def test_target_comments_blacklist(): + """Comments work for blacklist entries too.""" + from bbot.scanner.target import BBOTTarget + + target = BBOTTarget( + target=["evilcorp.com"], + blacklist=[ + "# don't scan the blog", + "blog.evilcorp.com # unstable host", + ], + ) + assert target.in_scope("www.evilcorp.com") + assert not target.in_scope("blog.evilcorp.com") + assert len(target.blacklist) == 1 + + +def test_target_comments_seeds(): + """Comments work for seed entries too.""" + from bbot.scanner.target import BBOTTarget + + target = BBOTTarget( + target=["evilcorp.com"], + seeds=[ + "# seed comment", + "evilcorp.com # the main domain", + ], + ) + assert "evilcorp.com" in target.seeds + assert len(target.seeds) == 1 + + +def test_target_comments_from_file(tmp_path): + """Comments in a target file are stripped when loaded via chain_lists.""" + from bbot.core.helpers.misc import chain_lists + + target_file = tmp_path / "targets.txt" + target_file.write_text( + "# My target list\n" + "evilcorp.com # main domain\n" + "\n" + " # another comment\n" + "othercorp.com\n" + "192.168.1.0/24 # lab network\n" + "http://example.com/page#fragment # with a URL fragment\n" + ) + + result = chain_lists([str(target_file)], try_files=True, _strip_comments=True) + assert "evilcorp.com" in result + assert "othercorp.com" in result + assert "192.168.1.0/24" in result + assert "http://example.com/page#fragment" in result + # comments and blank lines are gone + assert not any(r.lstrip().startswith("#") for r in result) + assert len(result) == 4 + + +def test_strip_comments_helper(): + """Unit tests for the strip_comments function.""" + from bbot.core.helpers.misc import strip_comments + + # full-line comments + assert strip_comments("# comment") == "" + assert strip_comments(" # indented comment") == "" + + # inline comments + assert strip_comments("evilcorp.com # main domain") == "evilcorp.com" + assert strip_comments("1.2.3.0/24\t# tab comment") == "1.2.3.0/24" + + # no comment + assert strip_comments("evilcorp.com") == "evilcorp.com" + + # URL fragment (no space before #) is preserved + assert strip_comments("http://example.com/page#section") == "http://example.com/page#section" + + # URL fragment with trailing inline comment + assert strip_comments("http://example.com/page#section # a comment") == "http://example.com/page#section" + + # empty / whitespace + assert strip_comments("") == "" + assert strip_comments(" ") == " " diff --git a/bbot/test/test_step_1/test_web.py b/bbot/test/test_step_1/test_web.py index 31fe34d7d5..fbaf6f6f97 100644 --- a/bbot/test/test_step_1/test_web.py +++ b/bbot/test/test_step_1/test_web.py @@ -16,6 +16,7 @@ def server_handler(request): bbot_httpserver.expect_request(uri=re.compile(r"/nope")).respond_with_data("nope", status=500) scan = bbot_scanner() + await scan._prep() # request response = await scan.helpers.request(f"{base_url}1") @@ -109,6 +110,7 @@ def server_handler(request): bbot_httpserver.expect_request(uri=re.compile(r"/test/\d+")).respond_with_handler(server_handler) scan = bbot_scanner() + await scan._prep() urls = [f"{base_url}{i}" for i in range(100)] @@ -135,6 +137,7 @@ def server_handler(request): async def test_web_helpers(bbot_scanner, bbot_httpserver, httpx_mock): # json conversion scan = bbot_scanner("evilcorp.com") + await scan._prep() url = "http://www.evilcorp.com/json_test?a=b" httpx_mock.add_response(url=url, text="hello\nworld") response = await scan.helpers.web.request(url) @@ -155,6 +158,7 @@ async def test_web_helpers(bbot_scanner, bbot_httpserver, httpx_mock): scan2 = bbot_scanner("127.0.0.1") await scan1._prep() + await scan2._prep() module = scan1.modules["ipneighbor"] web_config = CORE.config.get("web", {}) @@ -289,7 +293,8 @@ async def test_web_interactsh(bbot_scanner, bbot_httpserver): async_correct_url = False scan1 = bbot_scanner("8.8.8.8") - scan1.status = "RUNNING" + await scan1._prep() + await scan1._set_status("RUNNING") interactsh_client = scan1.helpers.interactsh(poll_interval=3) interactsh_client2 = scan1.helpers.interactsh(poll_interval=3) @@ -344,6 +349,7 @@ def sync_callback(data): @pytest.mark.asyncio async def test_web_curl(bbot_scanner, bbot_httpserver): scan = bbot_scanner("127.0.0.1") + await scan._prep() helpers = scan.helpers url = bbot_httpserver.url_for("/curl") bbot_httpserver.expect_request(uri="/curl").respond_with_data("curl_yep") @@ -379,6 +385,7 @@ async def test_web_curl(bbot_scanner, bbot_httpserver): @pytest.mark.asyncio async def test_web_http_compare(httpx_mock, bbot_scanner): scan = bbot_scanner() + await scan._prep() helpers = scan.helpers httpx_mock.add_response(url=re.compile(r"http://www\.example\.com.*"), text="wat") compare_helper = helpers.http_compare("http://www.example.com") @@ -402,6 +409,7 @@ async def test_http_proxy(bbot_scanner, bbot_httpserver, proxy_server): proxy_address = f"http://127.0.0.1:{proxy_server.server_address[1]}" scan = bbot_scanner("127.0.0.1", config={"web": {"http_proxy": proxy_address}}) + await scan._prep() assert len(proxy_server.RequestHandlerClass.urls) == 0 @@ -426,6 +434,8 @@ async def test_http_ssl(bbot_scanner, bbot_httpserver_ssl): scan1 = bbot_scanner("127.0.0.1", config={"web": {"ssl_verify": True, "debug": True}}) scan2 = bbot_scanner("127.0.0.1", config={"web": {"ssl_verify": False, "debug": True}}) + await scan1._prep() + await scan2._prep() r1 = await scan1.helpers.request(url) assert r1 is None, "Request to self-signed SSL server went through even with ssl_verify=True" @@ -445,6 +455,7 @@ async def test_web_cookies(bbot_scanner, httpx_mock): # make sure cookies work when enabled httpx_mock.add_response(url="http://www.evilcorp.com/cookies", headers=[("set-cookie", "wat=asdf; path=/")]) scan = bbot_scanner() + await scan._prep() client = BBOTAsyncClient(persist_cookies=True, _config=scan.config, _target=scan.target) r = await client.get(url="http://www.evilcorp.com/cookies") @@ -461,6 +472,7 @@ async def test_web_cookies(bbot_scanner, httpx_mock): # make sure they don't when they're not httpx_mock.add_response(url="http://www2.evilcorp.com/cookies", headers=[("set-cookie", "wats=fdsa; path=/")]) scan = bbot_scanner() + await scan._prep() client2 = BBOTAsyncClient(persist_cookies=False, _config=scan.config, _target=scan.target) r = await client2.get(url="http://www2.evilcorp.com/cookies") # make sure we can access the cookies @@ -493,6 +505,7 @@ def echo_cookies_handler(request): bbot_httpserver.expect_request(uri=endpoint).respond_with_handler(echo_cookies_handler) scan1 = bbot_scanner("127.0.0.1", config={"web": {"debug": True}}) + await scan1._prep() r1 = await scan1.helpers.request(url, cookies={"foo": "bar"}) assert r1 is not None, "Request to self-signed SSL server went through even with ssl_verify=True" diff --git a/bbot/test/test_step_2/module_tests/base.py b/bbot/test/test_step_2/module_tests/base.py index 26bd0b7995..ffb4ae0d46 100644 --- a/bbot/test/test_step_2/module_tests/base.py +++ b/bbot/test/test_step_2/module_tests/base.py @@ -15,7 +15,7 @@ class ModuleTestBase: targets = ["blacklanternsecurity.com"] scan_name = None blacklist = None - whitelist = None + seeds = None module_name = None config_overrides = {} modules_overrides = None @@ -53,13 +53,15 @@ def __init__( elif module_type == "internal" and not module == "dnsresolve": self.config = OmegaConf.merge(self.config, {module: True}) + seeds = module_test_base.seeds or None + self.scan = Scanner( *module_test_base.targets, modules=modules, output_modules=output_modules, scan_name=module_test_base._scan_name, config=self.config, - whitelist=module_test_base.whitelist, + seeds=seeds, blacklist=module_test_base.blacklist, force_start=getattr(module_test_base, "force_start", False), ) @@ -100,18 +102,27 @@ async def module_test( module_test = self.ModuleTest( self, httpx_mock, bbot_httpserver, bbot_httpserver_ssl, monkeypatch, request, caplog, capsys ) - self.log.debug("Mocking DNS") - await module_test.mock_dns({"blacklanternsecurity.com": {"A": ["127.0.0.88"]}}) self.log.debug("Executing setup_before_prep()") await self.setup_before_prep(module_test) self.log.debug("Executing scan._prep()") await module_test.scan._prep() + self.log.debug("Mocking DNS") + await module_test.mock_dns({"blacklanternsecurity.com": {"A": ["127.0.0.88"]}}) self.log.debug("Executing setup_after_prep()") await self.setup_after_prep(module_test) self.log.debug("Starting scan") await self._execute_scan(module_test) self.log.debug(f"Finished {module_test.name} module test") yield module_test + # Cancel any orphaned async tasks left by the test (e.g. pymongo, aio_pika background threads). + # These persist on the session-scoped event loop and can block subsequent test fixtures. + current_task = asyncio.current_task() + tasks = [t for t in asyncio.all_tasks() if t != current_task and not t.done()] + if tasks: + self.log.debug(f"Cancelling {len(tasks)} orphaned tasks after {self.name}") + for t in tasks: + t.cancel() + await asyncio.gather(*tasks, return_exceptions=True) async def _execute_scan(self, module_test): """Execute the scan and collect events. Can be overridden by benchmark classes.""" @@ -158,3 +169,19 @@ async def setup_before_prep(self, module_test): async def setup_after_prep(self, module_test): pass + + async def wait_for_port_open(self, port): + while not await self.is_port_open("localhost", port): + self.log.verbose(f"Waiting for port {port} to be open...") + await asyncio.sleep(0.5) + # allow an extra second for things to settle + await asyncio.sleep(1) + + async def is_port_open(self, host, port): + try: + reader, writer = await asyncio.open_connection(host, port) + writer.close() + await writer.wait_closed() + return True + except (ConnectionRefusedError, OSError): + return False diff --git a/bbot/test/test_step_2/module_tests/test_module_affiliates.py b/bbot/test/test_step_2/module_tests/test_module_affiliates.py index 68398ca480..820ef306fc 100644 --- a/bbot/test/test_step_2/module_tests/test_module_affiliates.py +++ b/bbot/test/test_step_2/module_tests/test_module_affiliates.py @@ -3,9 +3,9 @@ class TestAffiliates(ModuleTestBase): targets = ["8.8.8.8"] - config_overrides = {"dns": {"minimal": False}} + config_overrides = {"dns": {"minimal": False, "filter_ptrs": False}} - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): await module_test.mock_dns( { "8.8.8.8.in-addr.arpa": {"PTR": ["dns.google"]}, diff --git a/bbot/test/test_step_2/module_tests/test_module_aggregate.py b/bbot/test/test_step_2/module_tests/test_module_aggregate.py index 583fcaec79..ba1d2edd8a 100644 --- a/bbot/test/test_step_2/module_tests/test_module_aggregate.py +++ b/bbot/test/test_step_2/module_tests/test_module_aggregate.py @@ -4,7 +4,7 @@ class TestAggregate(ModuleTestBase): config_overrides = {"dns": {"minimal": False}, "scope": {"report_distance": 1}} - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): await module_test.mock_dns({"blacklanternsecurity.com": {"A": ["1.2.3.4"]}}) def check(self, module_test, events): diff --git a/bbot/test/test_step_2/module_tests/test_module_ajaxpro.py b/bbot/test/test_step_2/module_tests/test_module_ajaxpro.py index b917de42ca..7cbbbb783c 100644 --- a/bbot/test/test_step_2/module_tests/test_module_ajaxpro.py +++ b/bbot/test/test_step_2/module_tests/test_module_ajaxpro.py @@ -35,7 +35,7 @@ def check(self, module_test, events): for e in events: if ( - e.type == "VULNERABILITY" + e.type == "FINDING" and "Ajaxpro Deserialization RCE (CVE-2021-23758)" in e.data["description"] and "http://127.0.0.1:8888/ajaxpro/AjaxPro.Services.ICartService,AjaxPro.2.ashx" in e.data["description"] diff --git a/bbot/test/test_step_2/module_tests/test_module_asn.py b/bbot/test/test_step_2/module_tests/test_module_asn.py index fbd3558a43..1f74b776ce 100644 --- a/bbot/test/test_step_2/module_tests/test_module_asn.py +++ b/bbot/test/test_step_2/module_tests/test_module_asn.py @@ -1,239 +1,30 @@ from .base import ModuleTestBase -class TestASNBGPView(ModuleTestBase): +class TestASNHelper(ModuleTestBase): + """Test ASN module with real asndb lookup against Google's 8.8.8.8 (AS15169).""" + targets = ["8.8.8.8"] module_name = "asn" - config_overrides = {"scope": {"report_distance": 2}} - - response_get_asn_bgpview = { - "status": "ok", - "status_message": "Query was successful", - "data": { - "ip": "8.8.8.8", - "ptr_record": "dns.google", - "prefixes": [ - { - "prefix": "8.8.8.0/24", - "ip": "8.8.8.0", - "cidr": 24, - "asn": {"asn": 15169, "name": "GOOGLE", "description": "Google LLC", "country_code": "US"}, - "name": "LVLT-GOGL-8-8-8", - "description": "Google LLC", - "country_code": "US", - } - ], - "rir_allocation": { - "rir_name": "ARIN", - "country_code": None, - "ip": "8.0.0.0", - "cidr": 9, - "prefix": "8.0.0.0/9", - "date_allocated": "1992-12-01 00:00:00", - "allocation_status": "allocated", - }, - "iana_assignment": { - "assignment_status": "legacy", - "description": "Administered by ARIN", - "whois_server": "whois.arin.net", - "date_assigned": None, - }, - "maxmind": {"country_code": None, "city": None}, - }, - "@meta": {"time_zone": "UTC", "api_version": 1, "execution_time": "567.18 ms"}, - } - response_get_emails_bgpview = { - "status": "ok", - "status_message": "Query was successful", - "data": { - "asn": 15169, - "name": "GOOGLE", - "description_short": "Google LLC", - "description_full": ["Google LLC"], - "country_code": "US", - "website": "https://about.google/intl/en/", - "email_contacts": ["network-abuse@google.com", "arin-contact@google.com"], - "abuse_contacts": ["network-abuse@google.com"], - "looking_glass": None, - "traffic_estimation": None, - "traffic_ratio": "Mostly Outbound", - "owner_address": ["1600 Amphitheatre Parkway", "Mountain View", "CA", "94043", "US"], - "rir_allocation": { - "rir_name": "ARIN", - "country_code": "US", - "date_allocated": "2000-03-30 00:00:00", - "allocation_status": "assigned", - }, - "iana_assignment": { - "assignment_status": None, - "description": None, - "whois_server": None, - "date_assigned": None, - }, - "date_updated": "2023-02-07 06:39:11", - }, - "@meta": {"time_zone": "UTC", "api_version": 1, "execution_time": "56.55 ms"}, - } - - async def setup_after_prep(self, module_test): - module_test.httpx_mock.add_response( - url="https://api.bgpview.io/ip/8.8.8.8", json=self.response_get_asn_bgpview - ) - module_test.httpx_mock.add_response( - url="https://api.bgpview.io/asn/15169", json=self.response_get_emails_bgpview - ) - module_test.module.sources = ["bgpview"] + modules_overrides = ["asn", "speculate"] + config_overrides = {"scope": {"report_distance": 2}, "speculate": True} def check(self, module_test, events): - assert any(e.type == "ASN" for e in events) - assert any(e.type == "EMAIL_ADDRESS" for e in events) - + asn_events = [e for e in events if e.type == "ASN"] + assert asn_events, "No ASN event produced" + assert any(isinstance(e.data, dict) and e.data.get("asn", 0) == 15169 for e in asn_events) -class TestASNRipe(ModuleTestBase): - targets = ["8.8.8.8"] - module_name = "asn" - config_overrides = {"scope": {"report_distance": 2}} - response_get_asn_ripe = { - "messages": [], - "see_also": [], - "version": "1.1", - "data_call_name": "network-info", - "data_call_status": "supported", - "cached": False, - "data": {"asns": ["15169"], "prefix": "8.8.8.0/24"}, - "query_id": "20230217212133-f278ff23-d940-4634-8115-a64dee06997b", - "process_time": 5, - "server_id": "app139", - "build_version": "live.2023.2.1.142", - "status": "ok", - "status_code": 200, - "time": "2023-02-17T21:21:33.428469", - } - response_get_asn_metadata_ripe = { - "messages": [], - "see_also": [], - "version": "4.1", - "data_call_name": "whois", - "data_call_status": "supported - connecting to ursa", - "cached": False, - "data": { - "records": [ - [ - {"key": "ASNumber", "value": "15169", "details_link": None}, - {"key": "ASName", "value": "GOOGLE", "details_link": None}, - {"key": "ASHandle", "value": "15169", "details_link": "https://stat.ripe.net/AS15169"}, - {"key": "RegDate", "value": "2000-03-30", "details_link": None}, - { - "key": "Ref", - "value": "https://rdap.arin.net/registry/autnum/15169", - "details_link": "https://rdap.arin.net/registry/autnum/15169", - }, - {"key": "source", "value": "ARIN", "details_link": None}, - ], - [ - {"key": "OrgAbuseHandle", "value": "ABUSE5250-ARIN", "details_link": None}, - {"key": "OrgAbuseName", "value": "Abuse", "details_link": None}, - {"key": "OrgAbusePhone", "value": "+1-650-253-0000", "details_link": None}, - { - "key": "OrgAbuseEmail", - "value": "network-abuse@google.com", - "details_link": "mailto:network-abuse@google.com", - }, - { - "key": "OrgAbuseRef", - "value": "https://rdap.arin.net/registry/entity/ABUSE5250-ARIN", - "details_link": "https://rdap.arin.net/registry/entity/ABUSE5250-ARIN", - }, - {"key": "source", "value": "ARIN", "details_link": None}, - ], - [ - {"key": "OrgName", "value": "Google LLC", "details_link": None}, - {"key": "OrgId", "value": "GOGL", "details_link": None}, - {"key": "Address", "value": "1600 Amphitheatre Parkway", "details_link": None}, - {"key": "City", "value": "Mountain View", "details_link": None}, - {"key": "StateProv", "value": "CA", "details_link": None}, - {"key": "PostalCode", "value": "94043", "details_link": None}, - {"key": "Country", "value": "US", "details_link": None}, - {"key": "RegDate", "value": "2000-03-30", "details_link": None}, - { - "key": "Comment", - "value": "Please note that the recommended way to file abuse complaints are located in the following links.", - "details_link": None, - }, - { - "key": "Comment", - "value": "To report abuse and illegal activity: https://www.google.com/contact/", - "details_link": None, - }, - { - "key": "Comment", - "value": "For legal requests: http://support.google.com/legal", - "details_link": None, - }, - {"key": "Comment", "value": "Regards,", "details_link": None}, - {"key": "Comment", "value": "The Google Team", "details_link": None}, - { - "key": "Ref", - "value": "https://rdap.arin.net/registry/entity/GOGL", - "details_link": "https://rdap.arin.net/registry/entity/GOGL", - }, - {"key": "source", "value": "ARIN", "details_link": None}, - ], - [ - {"key": "OrgTechHandle", "value": "ZG39-ARIN", "details_link": None}, - {"key": "OrgTechName", "value": "Google LLC", "details_link": None}, - {"key": "OrgTechPhone", "value": "+1-650-253-0000", "details_link": None}, - { - "key": "OrgTechEmail", - "value": "arin-contact@google.com", - "details_link": "mailto:arin-contact@google.com", - }, - { - "key": "OrgTechRef", - "value": "https://rdap.arin.net/registry/entity/ZG39-ARIN", - "details_link": "https://rdap.arin.net/registry/entity/ZG39-ARIN", - }, - {"key": "source", "value": "ARIN", "details_link": None}, - ], - [ - {"key": "RTechHandle", "value": "ZG39-ARIN", "details_link": None}, - {"key": "RTechName", "value": "Google LLC", "details_link": None}, - {"key": "RTechPhone", "value": "+1-650-253-0000", "details_link": None}, - {"key": "RTechEmail", "value": "arin-contact@google.com", "details_link": None}, - { - "key": "RTechRef", - "value": "https://rdap.arin.net/registry/entity/ZG39-ARIN", - "details_link": None, - }, - {"key": "source", "value": "ARIN", "details_link": None}, - ], - ], - "irr_records": [], - "authorities": ["arin"], - "resource": "15169", - "query_time": "2023-02-17T21:25:00", - }, - "query_id": "20230217212529-75f57efd-59f4-473f-8bdd-803062e94290", - "process_time": 268, - "server_id": "app143", - "build_version": "live.2023.2.1.142", - "status": "ok", - "status_code": 200, - "time": "2023-02-17T21:25:29.417812", - } +class TestASNUnknownHandling(ModuleTestBase): + """Test that no ASN events are emitted for private IPs (unknown ASN).""" - async def setup_after_prep(self, module_test): - module_test.httpx_mock.add_response( - url="https://stat.ripe.net/data/network-info/data.json?resource=8.8.8.8", - json=self.response_get_asn_ripe, - ) - module_test.httpx_mock.add_response( - url="https://stat.ripe.net/data/whois/data.json?resource=15169", - json=self.response_get_asn_metadata_ripe, - ) - module_test.module.sources = ["ripe"] + targets = ["192.168.1.1"] + module_name = "asn" + modules_overrides = ["asn", "speculate"] + config_overrides = {"scope": {"report_distance": 2}, "speculate": True} def check(self, module_test, events): - assert any(e.type == "ASN" for e in events) - assert any(e.type == "EMAIL_ADDRESS" for e in events) + asn_events = [e for e in events if e.type == "ASN"] + assert not asn_events, ( + f"Should not emit any ASN events for private IP, but found: {[e.data for e in asn_events]}" + ) diff --git a/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py b/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py index ca14ff7d03..f86578ebac 100644 --- a/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py +++ b/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py @@ -62,12 +62,12 @@ async def setup_before_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - vulnerability_found = False + finding_found = False for e in events: - if e.type == "VULNERABILITY" and "IIS Bin Directory DLL Exposure" in e.data["description"]: - vulnerability_found = True + if e.type == "FINDING" and "IIS Bin Directory DLL Exposure" in e.data["description"]: + finding_found = True assert e.data["severity"] == "HIGH", "Vulnerability severity should be HIGH" assert "Detection Url" in e.data["description"], "Description should include detection URL" break - assert vulnerability_found, "No vulnerability event was found" + assert finding_found, "No finding event was found" diff --git a/bbot/test/test_step_2/module_tests/test_module_asset_inventory.py b/bbot/test/test_step_2/module_tests/test_module_asset_inventory.py index 5cb2f36033..39aca71341 100644 --- a/bbot/test/test_step_2/module_tests/test_module_asset_inventory.py +++ b/bbot/test/test_step_2/module_tests/test_module_asset_inventory.py @@ -9,7 +9,7 @@ class TestAsset_Inventory(ModuleTestBase): masscan_output = """{ "ip": "127.0.0.1", "timestamp": "1680197558", "ports": [ {"port": 9999, "proto": "tcp", "status": "open", "reason": "syn-ack", "ttl": 54} ] }""" - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): async def run_masscan(command, *args, **kwargs): if "masscan" in command[:2]: targets = open(command[11]).read().splitlines() diff --git a/bbot/test/test_step_2/module_tests/test_module_azure_realm.py b/bbot/test/test_step_2/module_tests/test_module_azure_realm.py deleted file mode 100644 index 2b1317629f..0000000000 --- a/bbot/test/test_step_2/module_tests/test_module_azure_realm.py +++ /dev/null @@ -1,32 +0,0 @@ -from .base import ModuleTestBase - - -class TestAzure_Realm(ModuleTestBase): - targets = ["evilcorp.com"] - config_overrides = {"scope": {"report_distance": 1}} - - response_json = { - "State": 3, - "UserState": 2, - "Login": "test@evilcorp.com", - "NameSpaceType": "Federated", - "DomainName": "evilcorp.com", - "FederationGlobalVersion": -1, - "AuthURL": "https://evilcorp.okta.com/app/office365/deadbeef/sso/wsfed/passive?username=test%40evilcorp.com&wa=wsignin1.0&wtrevilcorplm=urn%3afederation%3aMicrosoftOnline&wctx=", - "FederationBrandName": "EvilCorp", - "AuthNForwardType": 1, - "CloudInstanceName": "microsoftonline.com", - "CloudInstanceIssuerUri": "urn:federation:MicrosoftOnline", - } - - async def setup_after_prep(self, module_test): - await module_test.mock_dns({"evilcorp.com": {"A": ["127.0.0.5"]}}) - module_test.httpx_mock.add_response( - url="https://login.microsoftonline.com/getuserrealm.srf?login=test@evilcorp.com", - json=self.response_json, - ) - - def check(self, module_test, events): - assert any(e.data == "https://evilcorp.okta.com/app/office365/deadbeef/sso/wsfed/passive" for e in events), ( - "Failed to detect URL" - ) diff --git a/bbot/test/test_step_2/module_tests/test_module_azure_tenant.py b/bbot/test/test_step_2/module_tests/test_module_azure_tenant.py index ee15d1a6dd..787ecbdba1 100644 --- a/bbot/test/test_step_2/module_tests/test_module_azure_tenant.py +++ b/bbot/test/test_step_2/module_tests/test_module_azure_tenant.py @@ -1,7 +1,111 @@ from .base import ModuleTestBase -class TestAzure_Tenant(ModuleTestBase): +class AzureTenantTestBase(ModuleTestBase): + """Base class for Azure Tenant tests with common setup""" + + modules = ["azure_tenant"] + targets = ["blacklanternsecurity.com"] + + # Override these in subclasses to customize behavior + include_onmicrosoft = False + desktop_sso_enabled = False + certificate_auth_enabled = False + cloud_type = None # None, "dod", "gcc-high" + federation_url = None + exchange_online = False + directory_sync_enabled = False + + async def setup_after_prep(self, module_test): + # Build email domains + email_domains = ["blacklanternsecurity.com"] + if self.include_onmicrosoft: + email_domains.append("blacklanternsecurity.onmicrosoft.com") + + # Mock azmap.dev response + module_test.httpx_mock.add_response( + url="https://azmap.dev/api/tenant?domain=blacklanternsecurity.com&extract=true", + json={ + "tenant_id": "test-tenant-id", + "tenant_name": "blacklanternsecurity", + "email_domains": email_domains, + }, + ) + + # Mock ODC endpoint + module_test.httpx_mock.add_response( + url="https://odc.officeapps.live.com/odc/v2.1/federationprovider?domain=blacklanternsecurity.com", + json={}, + ) + + # Mock OpenID configuration + openid_config = {} + if self.cloud_type == "dod": + openid_config = { + "tenant_region_scope": "DOD", + "tenant_region_sub_scope": "DOD", + "cloud_instance_name": "login.microsoftonline.us", + } + elif self.cloud_type == "gcc-high": + openid_config = { + "tenant_region_scope": "DODCON", + "tenant_region_sub_scope": "DODCON", + "cloud_instance_name": "login.microsoftonline.us", + } + + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/blacklanternsecurity.com/.well-known/openid-configuration", + json=openid_config, + ) + + # Mock GetCredentialType + getcred_response = {"EstsProperties": {}, "Credentials": {}} + if self.desktop_sso_enabled: + getcred_response["EstsProperties"]["DesktopSsoEnabled"] = True + if self.certificate_auth_enabled: + getcred_response["Credentials"]["HasCertAuth"] = True + if self.federation_url: + getcred_response["Credentials"]["FederationRedirectUrl"] = self.federation_url + + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/common/GetCredentialType", + method="POST", + json=getcred_response, + ) + + # Mock UserRealm v2.0 + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/common/userrealm/test@blacklanternsecurity.com?api-version=2.0", + json={}, + ) + + # Mock MTA-STS + if self.exchange_online: + module_test.httpx_mock.add_response( + url="https://mta-sts.blacklanternsecurity.com/.well-known/mta-sts.txt", + text="version: STSv1\nmode: enforce\nmx: blacklanternsecurity-com.mail.protection.outlook.com\nmax_age: 604800", + ) + else: + module_test.httpx_mock.add_response( + url="https://mta-sts.blacklanternsecurity.com/.well-known/mta-sts.txt", + status_code=404, + ) + + # Mock Directory Sync check if needed + if self.include_onmicrosoft: + sync_result = 0 if self.directory_sync_enabled else 1 + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/common/GetCredentialType", + method="POST", + json={"IfExistsResult": sync_result}, + ) + + +class TestAzure_Tenant(AzureTenantTestBase): + """Test basic Azure tenant enumeration""" + + include_onmicrosoft = True + tenant_response = { "tenant_id": "cc74fc12-4142-400e-a653-f98bdeadbeef", "tenant_name": "blacklanternsecurity", @@ -15,11 +119,46 @@ class TestAzure_Tenant(ModuleTestBase): } async def setup_after_prep(self, module_test): + # Use custom response for this test module_test.httpx_mock.add_response( url="https://azmap.dev/api/tenant?domain=blacklanternsecurity.com&extract=true", json=self.tenant_response, ) + module_test.httpx_mock.add_response( + url="https://odc.officeapps.live.com/odc/v2.1/federationprovider?domain=blacklanternsecurity.com", + json={"TenantId": "cc74fc12-4142-400e-a653-f98bdeadbeef"}, + ) + + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/blacklanternsecurity.com/.well-known/openid-configuration", + json={ + "tenant_region_scope": "NA", + "cloud_instance_name": "microsoftonline.com", + }, + ) + + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/common/GetCredentialType", + json={"EstsProperties": {}, "Credentials": {}}, + ) + + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/common/userrealm/test@blacklanternsecurity.com?api-version=2.0", + json={"NameSpaceType": "Managed"}, + ) + + module_test.httpx_mock.add_response( + url="https://mta-sts.blacklanternsecurity.com/.well-known/mta-sts.txt", + status_code=404, + ) + + # Directory sync check + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/common/GetCredentialType", + json={"IfExistsResult": 1}, + ) + def check(self, module_test, events): assert any( e.type.startswith("DNS_NAME") @@ -34,3 +173,146 @@ def check(self, module_test, events): and "blacklanternsecurity" in e.data["tenant-names"] for e in events ) + + +class TestAzure_Tenant_DesktopSSO(AzureTenantTestBase): + """Test Desktop SSO detection and FINDING emission""" + + desktop_sso_enabled = True + + def check(self, module_test, events): + assert any(e.type == "AZURE_TENANT" and e.data.get("desktop-sso-enabled") is True for e in events), ( + "AZURE_TENANT should have desktop-sso-enabled=True" + ) + + assert any( + e.type == "FINDING" + and e.data.get("name") == "Azure AD Desktop SSO Enabled" + and e.data.get("severity") == "INFO" + and e.data.get("confidence") == "MEDIUM" + and "azure-sso" in e.tags + for e in events + ), "Should emit Desktop SSO FINDING with correct severity and confidence" + + +class TestAzure_Tenant_CertificateAuth(AzureTenantTestBase): + """Test Certificate-Based Authentication detection""" + + certificate_auth_enabled = True + + def check(self, module_test, events): + assert any(e.type == "AZURE_TENANT" and e.data.get("certificate-auth-enabled") is True for e in events), ( + "AZURE_TENANT should have certificate-auth-enabled=True" + ) + + assert any( + e.type == "FINDING" + and e.data.get("name") == "Certificate-Based Authentication Enabled" + and e.data.get("severity") == "INFO" + and e.data.get("confidence") == "HIGH" + and "azure-cba" in e.tags + for e in events + ), "Should emit Certificate Auth FINDING with correct severity and confidence" + + +class TestAzure_Tenant_GovCloud(AzureTenantTestBase): + """Test Government Cloud detection (DoD)""" + + cloud_type = "dod" + + def check(self, module_test, events): + assert any(e.type == "AZURE_TENANT" and e.data.get("cloud-type") == "dod" for e in events), ( + "AZURE_TENANT should have cloud-type=dod" + ) + + assert any( + e.type == "FINDING" + and e.data.get("name") == "Azure Government Cloud Tenant" + and e.data.get("severity") == "INFO" + and e.data.get("confidence") == "HIGH" + and "azure-gov-cloud" in e.tags + for e in events + ), "Should emit Government Cloud FINDING with HIGH severity" + + +class TestAzure_Tenant_GCCHigh(AzureTenantTestBase): + """Test GCC High Government Cloud detection""" + + cloud_type = "gcc-high" + + def check(self, module_test, events): + assert any(e.type == "AZURE_TENANT" and e.data.get("cloud-type") == "gcc-high" for e in events), ( + "AZURE_TENANT should have cloud-type=gcc-high" + ) + + assert any( + e.type == "FINDING" + and e.data.get("name") == "Azure Government Cloud Tenant" + and "gcc-high" in e.data.get("description", "") + and "azure-gov-cloud" in e.tags + for e in events + ), "Should emit Government Cloud FINDING for GCC High" + + +class TestAzure_Tenant_DirectorySync(AzureTenantTestBase): + """Test Directory Synchronization detection""" + + include_onmicrosoft = True + directory_sync_enabled = True + + def check(self, module_test, events): + assert any(e.type == "AZURE_TENANT" and e.data.get("directory-sync-enabled") is True for e in events), ( + "AZURE_TENANT should have directory-sync-enabled=True" + ) + + assert any( + e.type == "FINDING" + and e.data.get("name") == "Directory Synchronization Enabled" + and e.data.get("severity") == "INFO" + and e.data.get("confidence") == "HIGH" + and "azure-dir-sync" in e.tags + for e in events + ), "Should emit Directory Sync FINDING" + + +class TestAzure_Tenant_FederatedAuth(AzureTenantTestBase): + """Test Federated Authentication detection""" + + federation_url = "https://authfs.example.com/adfs/ls/?username=test%40blacklanternsecurity.com" + + def check(self, module_test, events): + assert any( + e.type == "AZURE_TENANT" and e.data.get("federation-redirect-url") == self.federation_url for e in events + ), "AZURE_TENANT should have federation-redirect-url" + + assert any( + e.type == "FINDING" + and e.data.get("name") == "Federated Authentication Detected" + and e.data.get("severity") == "INFO" + and e.data.get("confidence") == "HIGH" + and e.data.get("full_url") == self.federation_url # full_url preserves query string + and e.data.get("url") == "https://authfs.example.com/adfs/ls/" # url is cleaned + and "azure-federated" in e.tags + for e in events + ), "Should emit Federated Auth FINDING with INFO severity" + + # URL_UNVERIFIED also gets cleaned (query string removed) + assert any( + e.type == "URL_UNVERIFIED" and e.url == "https://authfs.example.com/adfs/ls/" and "ms-auth-url" in e.tags + for e in events + ), "Should emit URL_UNVERIFIED for federation URL" + + +class TestAzure_Tenant_ExchangeOnline(AzureTenantTestBase): + """Test Exchange Online detection via MTA-STS""" + + exchange_online = True + + def check(self, module_test, events): + assert any(e.type == "AZURE_TENANT" and e.data.get("exchange-online") is True for e in events), ( + "AZURE_TENANT should have exchange-online=True" + ) + + assert any(e.type == "AZURE_TENANT" and e.data.get("mta-sts-mode") == "enforce" for e in events), ( + "AZURE_TENANT should have mta-sts-mode" + ) diff --git a/bbot/test/test_step_2/module_tests/test_module_baddns.py b/bbot/test/test_step_2/module_tests/test_module_baddns.py index 877e973b2b..47d38d989b 100644 --- a/bbot/test/test_step_2/module_tests/test_module_baddns.py +++ b/bbot/test/test_step_2/module_tests/test_module_baddns.py @@ -1,6 +1,146 @@ +import ast +import inspect + +from baddns.base import get_all_modules +from baddns.lib.findings import SEVERITY_LEVELS, CONFIDENCE_LEVELS + +from bbot.modules.baddns import SUBMODULE_MAX_SEVERITY, SUBMODULE_MAX_CONFIDENCE + from .base import ModuleTestBase +def _extract_finding_values(module_cls): + """ + AST-parse a baddns submodule and extract all string-literal severity + and confidence values from Finding() constructor calls. + + Returns (set of severity strings, set of confidence strings). + """ + source = inspect.getsource(module_cls) + tree = ast.parse(source) + + severities = set() + confidences = set() + + for node in ast.walk(tree): + # Look for Finding({...}) calls + if not isinstance(node, ast.Call): + continue + func = node.func + if not (isinstance(func, ast.Name) and func.id == "Finding"): + continue + if not node.args: + continue + arg = node.args[0] + if not isinstance(arg, ast.Dict): + continue + for key, value in zip(arg.keys, arg.values): + if not isinstance(key, ast.Constant): + continue + if key.value == "severity" and isinstance(value, ast.Constant) and isinstance(value.value, str): + severities.add(value.value) + if key.value == "confidence" and isinstance(value, ast.Constant) and isinstance(value.value, str): + confidences.add(value.value) + + return severities, confidences + + +def _max_level(values, levels): + """Return the highest value from `values` according to the ordering in `levels`.""" + if not values: + return None + # levels is ordered high-to-low in baddns (CRITICAL, HIGH, MEDIUM, LOW, INFO) + for level in levels: + if level in values: + return level + return None + + +# Modules that wrap CNAME findings and may use variables for severity/confidence. +# When a field uses a variable instead of a literal, its max is bounded by +# CNAME's max for that field. +CNAME_DERIVED_MODULES = {"references", "txt", "wildcard", "mtasts"} + + +def test_baddns_max_severity_confidence(): + """ + Ensure SUBMODULE_MAX_SEVERITY and SUBMODULE_MAX_CONFIDENCE in the BBOT + baddns module match what the baddns library's source code actually contains. + """ + all_modules = {cls.name: cls for cls in get_all_modules()} + + # First, compute CNAME's maxes since other modules derive from it + cname_cls = all_modules.get("CNAME") + assert cname_cls is not None, "CNAME module not found in baddns" + cname_sevs, cname_confs = _extract_finding_values(cname_cls) + cname_max_sev = _max_level(cname_sevs, SEVERITY_LEVELS) + cname_max_conf = _max_level(cname_confs, CONFIDENCE_LEVELS) + + errors = [] + + for name, cls in all_modules.items(): + sevs, confs = _extract_finding_values(cls) + + # For CNAME-derived modules, if no literal severity/confidence was found, + # the values are inherited from CNAME findings + if ( + name.lower() in CNAME_DERIVED_MODULES + or cls.__name__.lower().replace("baddns_", "") in CNAME_DERIVED_MODULES + ): + if not sevs and cname_max_sev: + sevs = {cname_max_sev} + if not confs and cname_max_conf: + confs = {cname_max_conf} + + actual_max_sev = _max_level(sevs, SEVERITY_LEVELS) + actual_max_conf = _max_level(confs, CONFIDENCE_LEVELS) + + expected_sev = SUBMODULE_MAX_SEVERITY.get(name) + expected_conf = SUBMODULE_MAX_CONFIDENCE.get(name) + + if expected_sev is None and expected_conf is None: + errors.append(f"Module [{name}] is missing from both SUBMODULE_MAX_SEVERITY and SUBMODULE_MAX_CONFIDENCE") + continue + + if expected_sev != actual_max_sev: + errors.append( + f"Module [{name}] max_severity mismatch: BBOT says {expected_sev}, baddns source says {actual_max_sev}" + ) + if expected_conf != actual_max_conf: + errors.append( + f"Module [{name}] max_confidence mismatch: BBOT says {expected_conf}, baddns source says {actual_max_conf}" + ) + + assert not errors, "BBOT baddns max severity/confidence out of sync with baddns library:\n" + "\n".join(errors) + + +def test_baddns_submodule_coverage(): + """ + Ensure every baddns submodule is represented in both SUBMODULE_MAX dicts, + and no stale entries exist for modules that were removed. + """ + all_module_names = {cls.name for cls in get_all_modules()} + severity_names = set(SUBMODULE_MAX_SEVERITY.keys()) + confidence_names = set(SUBMODULE_MAX_CONFIDENCE.keys()) + + missing_severity = all_module_names - severity_names + missing_confidence = all_module_names - confidence_names + stale_severity = severity_names - all_module_names + stale_confidence = confidence_names - all_module_names + + errors = [] + if missing_severity: + errors.append(f"Modules missing from SUBMODULE_MAX_SEVERITY: {missing_severity}") + if missing_confidence: + errors.append(f"Modules missing from SUBMODULE_MAX_CONFIDENCE: {missing_confidence}") + if stale_severity: + errors.append(f"Stale entries in SUBMODULE_MAX_SEVERITY (module removed from baddns): {stale_severity}") + if stale_confidence: + errors.append(f"Stale entries in SUBMODULE_MAX_CONFIDENCE (module removed from baddns): {stale_confidence}") + + assert not errors, "\n".join(errors) + + class BaseTestBaddns(ModuleTestBase): modules_overrides = ["baddns"] targets = ["bad.dns"] @@ -32,7 +172,7 @@ async def setup_after_prep(self, module_test): def check(self, module_test, events): assert any(e.data == "baddns.azurewebsites.net" for e in events), "CNAME detection failed" - assert any(e.type == "VULNERABILITY" for e in events), "Failed to emit VULNERABILITY" + assert any(e.type == "FINDING" for e in events), "Failed to emit FINDING" assert any("baddns-cname" in e.tags for e in events), "Failed to add baddns tag" @@ -61,7 +201,7 @@ def set_target(self, target): def check(self, module_test, events): assert any(e for e in events) - assert any(e.type == "VULNERABILITY" and "bigcartel.com" in e.data["description"] for e in events), ( - "Failed to emit VULNERABILITY" + assert any(e.type == "FINDING" and "bigcartel.com" in e.data["description"] for e in events), ( + "Failed to emit FINDING" ) assert any("baddns-cname" in e.tags for e in events), "Failed to add baddns tag" diff --git a/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py b/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py index b2b49717c8..cbf1a8c2d4 100644 --- a/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py +++ b/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py @@ -26,7 +26,7 @@ async def handle_event(self, event): self.events_seen.append(event.data) url = "http://bad.dns:8888/" url_event = self.scan.make_event( - url, "URL", parent=self.scan.root_event, tags=["cdn-cloudflare", "in-scope", "status-401"] + url, "URL", parent=self.scan.root_event, tags=["cloudflare", "in-scope", "status-401"] ) if url_event is not None: await self.emit_event(url_event) @@ -44,7 +44,10 @@ def set_target(self, target): module_test.scan.modules["dummy_module"] = self.dummy_module expect_args = {"method": "GET", "uri": "/"} - respond_args = {"response_data": "The specified bucket does not exist", "status": 401} + respond_args = { + "response_data": "NoSuchBucketThe specified bucket does not existbad.dns", + "status": 401, + } module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) await module_test.mock_dns({"bad.dns": {"A": ["127.0.0.1"]}}) @@ -55,7 +58,7 @@ def set_target(self, target): def check(self, module_test, events): assert any( e.type == "FINDING" - and "Possible [AWS Bucket Takeover Detection] via direct BadDNS analysis. Indicator: [[Words: The specified bucket does not exist | Condition: and | Part: body] Matchers-Condition: and] Trigger: [self] baddns Module: [CNAME]" + and "Possible [AWS Bucket Takeover Detection] via direct BadDNS analysis. Indicator: [[Words: The specified bucket does not exist, BucketName | Condition: and | Part: body] Matchers-Condition: and] Trigger: [self] baddns Module: [CNAME]" in e.data["description"] for e in events ), "Failed to emit FINDING" diff --git a/bbot/test/test_step_2/module_tests/test_module_baddns_zone.py b/bbot/test/test_step_2/module_tests/test_module_baddns_zone.py index d8138a3f7c..688f105ea5 100644 --- a/bbot/test/test_step_2/module_tests/test_module_baddns_zone.py +++ b/bbot/test/test_step_2/module_tests/test_module_baddns_zone.py @@ -38,25 +38,27 @@ def from_xfr(*args, **kwargs): def check(self, module_test, events): assert any(e.data == "zzzz.bad.dns" for e in events), "Zone transfer failed (1)" assert any(e.data == "asdf.bad.dns" for e in events), "Zone transfer failed (2)" - assert any(e.type == "VULNERABILITY" for e in events), "Failed to emit VULNERABILITY" + assert any(e.type == "FINDING" for e in events), "Failed to emit FINDING" assert any("baddns-zonetransfer" in e.tags for e in events), "Failed to add baddns tag" class TestBaddns_zone_nsec(BaseTestBaddns_zone): + targets = ["bad.com"] + async def setup_after_prep(self, module_test): from baddns.lib.whoismanager import WhoisManager await module_test.mock_dns( { - "bad.dns": {"A": ["127.0.0.5"], "NSEC": ["asdf.bad.dns"]}, - "asdf.bad.dns": {"NSEC": ["zzzz.bad.dns"]}, - "zzzz.bad.dns": {"NSEC": ["xyz.bad.dns"]}, + "bad.com": {"A": ["127.0.0.5"], "NSEC": ["asdf.bad.com"]}, + "asdf.bad.com": {"NSEC": ["zzzz.bad.com"]}, + "zzzz.bad.com": {"NSEC": ["xyz.bad.com"]}, } ) module_test.monkeypatch.setattr(WhoisManager, "dispatchWHOIS", self.dispatchWHOIS) def check(self, module_test, events): - assert any(e.data == "zzzz.bad.dns" for e in events), "NSEC Walk Failed (1)" - assert any(e.data == "xyz.bad.dns" for e in events), "NSEC Walk Failed (2)" - assert any(e.type == "VULNERABILITY" for e in events), "Failed to emit VULNERABILITY" + assert any(e.data == "zzzz.bad.com" for e in events), "NSEC Walk Failed (1)" + assert any(e.data == "xyz.bad.com" for e in events), "NSEC Walk Failed (2)" + assert any(e.type == "FINDING" for e in events), "Failed to emit FINDING" assert any("baddns-nsec" in e.tags for e in events), "Failed to add baddns tag" diff --git a/bbot/test/test_step_2/module_tests/test_module_badsecrets.py b/bbot/test/test_step_2/module_tests/test_module_badsecrets.py index 9eda654eb6..e5143767d2 100644 --- a/bbot/test/test_step_2/module_tests/test_module_badsecrets.py +++ b/bbot/test/test_step_2/module_tests/test_module_badsecrets.py @@ -79,7 +79,7 @@ def check(self, module_test, events): for e in events: if ( - e.type == "VULNERABILITY" + e.type == "FINDING" and "Known Secret Found." in e.data["description"] and "validationKey: 0F97BAE23F6F36801ABDB5F145124E00A6F795A97093D778EE5CD24F35B78B6FC4C0D0D4420657689C4F321F8596B59E83F02E296E970C4DEAD2DFE226294979 validationAlgo: SHA1 encryptionKey: 8CCFBC5B7589DD37DC3B4A885376D7480A69645DAEEC74F418B4877BEC008156 encryptionAlgo: AES" in e.data["description"] @@ -94,7 +94,7 @@ def check(self, module_test, events): IdentifyOnly = True if ( - e.type == "VULNERABILITY" + e.type == "FINDING" and "1234" in e.data["description"] and "eyJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkJhZFNlY3JldHMiLCJleHAiOjE1OTMxMzM0ODMsImlhdCI6MTQ2NjkwMzA4M30.ovqRikAo_0kKJ0GVrAwQlezymxrLGjcEiW_s3UJMMCo" in e.data["description"] @@ -102,7 +102,7 @@ def check(self, module_test, events): CookieBasedDetection = True if ( - e.type == "VULNERABILITY" + e.type == "FINDING" and "keyboard cat" in e.data["description"] and "s%3A8FnPwdeM9kdGTZlWvdaVtQ0S1BCOhY5G.qys7H2oGSLLdRsEq7sqh7btOohHsaRKqyjV4LiVnBvc" in e.data["description"] @@ -110,7 +110,7 @@ def check(self, module_test, events): CookieBasedDetection_2 = True if ( - e.type == "VULNERABILITY" + e.type == "FINDING" and "Express.js Secret (cookie-session)" in e.data["description"] and "zOQU7v7aTe_3zu7tnVuHi1MJ2DU" in e.data["description"] ): @@ -122,6 +122,66 @@ def check(self, module_test, events): assert CookieBasedDetection_2, "No Express.js cookie vuln detected" assert CookieBasedDetection_3, "No Express.js (cs dual cookies) vuln detected" + # Verify that badsecrets emits CONFIRMED confidence for detected secrets + confirmed_finding = None + for e in events: + if e.type == "FINDING" and "Known Secret Found." in e.data["description"]: + confirmed_finding = e + break + if confirmed_finding: + assert confirmed_finding.data["confidence"] == "CONFIRMED" + + +class TestBadSecrets_JWTIdentifyOnly(ModuleTestBase): + """Verify that badsecrets suppresses JWT IdentifyOnly findings (excavate handles JWT detection) + while still emitting SecretFound findings for vulnerable JWTs.""" + + targets = [ + "http://127.0.0.1:8888/", + "http://127.0.0.1:8888/vuln_jwt.aspx", + "http://127.0.0.1:8888/safe_jwt.aspx", + ] + modules_overrides = ["badsecrets", "httpx"] + + # JWT signed with a secret NOT in badsecrets' wordlists (will produce IdentifyOnly) + safe_jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0In0.BHvEIdlrTFS4VXvT9nUOycVzokhfIYSxJa7DXNz_h0o" + + # JWT signed with "1234" which IS in badsecrets' wordlists (will produce SecretFound) + vuln_jwt = "eyJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkJhZFNlY3JldHMiLCJleHAiOjE1OTMxMzM0ODMsImlhdCI6MTQ2NjkwMzA4M30.ovqRikAo_0kKJ0GVrAwQlezymxrLGjcEiW_s3UJMMCo" + + async def setup_after_prep(self, module_test): + module_test.set_expect_requests( + expect_args={"uri": "/vuln_jwt.aspx"}, + respond_args={ + "response_data": "

Vulnerable JWT

", + "headers": {"set-cookie": f"vulnjwt={self.vuln_jwt}; secure"}, + }, + ) + module_test.set_expect_requests( + expect_args={"uri": "/safe_jwt.aspx"}, + respond_args={ + "response_data": "

Safe JWT

", + "headers": {"set-cookie": f"safejwt={self.safe_jwt}; secure"}, + }, + ) + module_test.set_expect_requests( + respond_args={"response_data": "index"}, + ) + + def check(self, module_test, events): + # Vulnerable JWT (SecretFound) should still produce a FINDING + assert any( + e.type == "FINDING" + and "Known Secret Found." in e.data["description"] + and self.vuln_jwt in e.data["description"] + for e in events + ), "Vulnerable JWT SecretFound finding was not emitted" + + # Safe JWT (IdentifyOnly) should NOT produce any FINDING + assert not any(e.type == "FINDING" and self.safe_jwt in e.data["description"] for e in events), ( + "JWT IdentifyOnly finding should have been suppressed" + ) + class TestBadSecrets_customsecrets(TestBadSecrets): config_overrides = { @@ -156,7 +216,7 @@ def check(self, module_test, events): SecretFound = False for e in events: if ( - e.type == "VULNERABILITY" + e.type == "FINDING" and "Known Secret Found." in e.data["description"] and "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" in e.data["description"] ): diff --git a/bbot/test/test_step_2/module_tests/test_module_bevigil.py b/bbot/test/test_step_2/module_tests/test_module_bevigil.py index 7e616752fa..7f1cb5d79d 100644 --- a/bbot/test/test_step_2/module_tests/test_module_bevigil.py +++ b/bbot/test/test_step_2/module_tests/test_module_bevigil.py @@ -25,7 +25,7 @@ async def setup_after_prep(self, module_test): def check(self, module_test, events): assert any(e.data == "asdf.blacklanternsecurity.com" for e in events), "Failed to detect subdomain" - assert any(e.data == "https://asdf.blacklanternsecurity.com/" for e in events), "Failed to detect url" + assert any(e.url == "https://asdf.blacklanternsecurity.com/" for e in events), "Failed to detect url" class TestBeVigilMultiKey(TestBeVigil): diff --git a/bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py b/bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py index 692b571e36..55220da36b 100644 --- a/bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +++ b/bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py @@ -75,33 +75,16 @@ def check(self, module_test, events): storage_buckets = [e for e in events if e.type == "STORAGE_BUCKET"] assert len(storage_buckets) == 3 assert 1 == len( - [ - e - for e in storage_buckets - if e.data["name"] == random_bucket_name_1 - and str(e.module) == "cloudcheck" - and f"cloud-{self.provider}" in e.tags - and f"{self.provider}-domain" in e.tags - ] + [e for e in storage_buckets if e.data["name"] == random_bucket_name_1 and str(e.module) == "cloudcheck"] ) assert 1 == len( - [ - e - for e in storage_buckets - if e.data["name"] == random_bucket_name_2 - and str(e.module) == "cloudcheck" - and f"cloud-{self.provider}" in e.tags - and f"{self.provider}-domain" in e.tags - ] + [e for e in storage_buckets if e.data["name"] == random_bucket_name_2 and str(e.module) == "cloudcheck"] ) assert 1 == len( [ e for e in storage_buckets - if e.data["name"] == random_bucket_name_3 - and str(e.module) == str(self.module_name) - and f"cloud-{module_test.module.cloudcheck_provider_name.lower()}" in e.tags - and f"{module_test.module.cloudcheck_provider_name.lower()}-domain" in e.tags + if e.data["name"] == random_bucket_name_3 and str(e.module) == str(self.module_name) ] ) # make sure open buckets were found diff --git a/bbot/test/test_step_2/module_tests/test_module_bucket_file_enum.py b/bbot/test/test_step_2/module_tests/test_module_bucket_file_enum.py index fa0719141b..da939222ed 100644 --- a/bbot/test/test_step_2/module_tests/test_module_bucket_file_enum.py +++ b/bbot/test/test_step_2/module_tests/test_module_bucket_file_enum.py @@ -39,7 +39,7 @@ async def setup_before_prep(self, module_test): def check(self, module_test, events): files = list((self.download_dir / "filedownload").glob("*.pdf")) - assert any(e.type == "URL_UNVERIFIED" and e.data.endswith("test.pdf") for e in events) - assert not any(e.type == "URL_UNVERIFIED" and e.data.endswith("test.css") for e in events) + assert any(e.type == "URL_UNVERIFIED" and e.url.endswith("test.pdf") for e in events) + assert not any(e.type == "URL_UNVERIFIED" and e.url.endswith("test.css") for e in events) assert any(f.name.endswith("test.pdf") for f in files), "Failed to download PDF file from open bucket" assert not any(f.name.endswith("test.css") for f in files), "Unwanted CSS file was downloaded" diff --git a/bbot/test/test_step_2/module_tests/test_module_bucket_microsoft.py b/bbot/test/test_step_2/module_tests/test_module_bucket_microsoft.py index 463f79033b..87ea18a440 100644 --- a/bbot/test/test_step_2/module_tests/test_module_bucket_microsoft.py +++ b/bbot/test/test_step_2/module_tests/test_module_bucket_microsoft.py @@ -21,7 +21,7 @@ class TestBucket_Microsoft_NoDup(ModuleTestBase): module_name = "bucket_microsoft" config_overrides = {"cloudcheck": True} - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( url="https://tesla.blob.core.windows.net/tesla?restype=container", text="", @@ -40,7 +40,7 @@ def check(self, module_test, events): assert bucket_event.data["url"] == "https://tesla.blob.core.windows.net/" assert ( bucket_event.discovery_context - == f"bucket_azure tried bucket variations of {event.data} and found {{event.type}} at {url}" + == "bucket_azure tried 3 bucket variations of tesla.com and found STORAGE_BUCKET at https://tesla.blob.core.windows.net/tesla?restype=container" ) @@ -50,6 +50,9 @@ class TestBucket_Microsoft_NoDup(TestBucket_Microsoft_NoDup): """ async def setup_after_prep(self, module_test): + # Call parent setup first + await super().setup_after_prep(module_test) + from bbot.core.event.base import STORAGE_BUCKET module_test.monkeypatch.setattr(STORAGE_BUCKET, "_suppress_chain_dupes", False) diff --git a/bbot/test/test_step_2/module_tests/test_module_c99.py b/bbot/test/test_step_2/module_tests/test_module_c99.py index ce9c7c8878..5721776483 100644 --- a/bbot/test/test_step_2/module_tests/test_module_c99.py +++ b/bbot/test/test_step_2/module_tests/test_module_c99.py @@ -69,8 +69,8 @@ def check(self, module_test, events): class TestC99AbortThreshold2(TestC99AbortThreshold1): targets = ["blacklanternsecurity.com", "evilcorp.com"] - async def setup_before_prep(self, module_test): - await super().setup_before_prep(module_test) + async def setup_after_prep(self, module_test): + await super().setup_after_prep(module_test) await module_test.mock_dns( { "blacklanternsecurity.com": {"A": ["127.0.0.88"]}, diff --git a/bbot/test/test_step_2/module_tests/test_module_censys_ip.py b/bbot/test/test_step_2/module_tests/test_module_censys_ip.py index 4aff673486..a0a879d5ae 100644 --- a/bbot/test/test_step_2/module_tests/test_module_censys_ip.py +++ b/bbot/test/test_step_2/module_tests/test_module_censys_ip.py @@ -6,25 +6,6 @@ class TestCensys_IP(ModuleTestBase): config_overrides = {"modules": {"censys_ip": {"api_key": "api_id:api_secret"}}} async def setup_before_prep(self, module_test): - await module_test.mock_dns( - { - "wildcard.evilcorp.com": { - "A": ["1.2.3.4"], - }, - "certname.evilcorp.com": { - "A": ["1.2.3.4"], - }, - "certsubject.evilcorp.com": { - "A": ["1.2.3.4"], - }, - "reversedns.evilcorp.com": { - "A": ["1.2.3.4"], - }, - "ptr.evilcorp.com": { - "A": ["1.2.3.4"], - }, - } - ) module_test.httpx_mock.add_response( url="https://search.censys.io/api/v1/account", match_headers={"Authorization": "Basic YXBpX2lkOmFwaV9zZWNyZXQ="}, @@ -135,6 +116,27 @@ async def setup_before_prep(self, module_test): }, ) + async def setup_after_prep(self, module_test): + await module_test.mock_dns( + { + "wildcard.evilcorp.com": { + "A": ["1.2.3.4"], + }, + "certname.evilcorp.com": { + "A": ["1.2.3.4"], + }, + "certsubject.evilcorp.com": { + "A": ["1.2.3.4"], + }, + "reversedns.evilcorp.com": { + "A": ["1.2.3.4"], + }, + "ptr.evilcorp.com": { + "A": ["1.2.3.4"], + }, + } + ) + def check(self, module_test, events): # Check OPEN_UDP_PORT event for DNS assert any(e.type == "OPEN_UDP_PORT" and e.data == "1.2.3.4:53" for e in events), ( @@ -161,13 +163,13 @@ def check(self, module_test, events): ) # Check URL_UNVERIFIED events - assert any(e.type == "URL_UNVERIFIED" and e.data == "http://1.2.3.4/" for e in events), ( + assert any(e.type == "URL_UNVERIFIED" and e.url == "http://1.2.3.4/" for e in events), ( "Failed to detect HTTP URL" ) - assert any(e.type == "URL_UNVERIFIED" and e.data == "https://1.2.3.4/" for e in events), ( + assert any(e.type == "URL_UNVERIFIED" and e.url == "https://1.2.3.4/" for e in events), ( "Failed to detect HTTPS URL" ) - assert any(e.type == "URL_UNVERIFIED" and e.data == "https://1.2.3.4:8443/admin" for e in events), ( + assert any(e.type == "URL_UNVERIFIED" and e.url == "https://1.2.3.4:8443/admin" for e in events), ( "Failed to detect HTTPS URL on port 8443" ) @@ -197,7 +199,7 @@ def check(self, module_test, events): e.type == "TECHNOLOGY" and e.data["technology"] == "cpe:2.3:a:apache:tomcat:9.0.50:*:*:*:*:*:*:*" for e in events ), "Failed to detect Apache Tomcat technology with CPE" - assert any(e.type == "TECHNOLOGY" and e.data["technology"] == "Java" for e in events), ( + assert any(e.type == "TECHNOLOGY" and e.data["technology"] == "java" for e in events), ( "Failed to detect Java technology without CPE" ) @@ -226,7 +228,6 @@ class TestCensys_IP_InScopeOnly(ModuleTestBase): config_overrides = {"modules": {"censys_ip": {"api_key": "api_id:api_secret", "in_scope_only": True}}} async def setup_before_prep(self, module_test): - await module_test.mock_dns({"evilcorp.com": {"A": ["1.1.1.1"]}}) module_test.httpx_mock.add_response( url="https://search.censys.io/api/v1/account", match_headers={"Authorization": "Basic YXBpX2lkOmFwaV9zZWNyZXQ="}, @@ -248,6 +249,9 @@ async def setup_before_prep(self, module_test): }, ) + async def setup_after_prep(self, module_test): + await module_test.mock_dns({"evilcorp.com": {"A": ["1.1.1.1"]}}) + def check(self, module_test, events): # Should NOT have queried the IP since it's out of scope assert not any(e.type == "OPEN_TCP_PORT" and "1.1.1.1" in e.data for e in events), ( @@ -267,7 +271,6 @@ class TestCensys_IP_OutOfScope(ModuleTestBase): } async def setup_before_prep(self, module_test): - await module_test.mock_dns({"evilcorp.com": {"A": ["1.1.1.1"]}}) module_test.httpx_mock.add_response( url="https://search.censys.io/api/v1/account", match_headers={"Authorization": "Basic YXBpX2lkOmFwaV9zZWNyZXQ="}, @@ -289,6 +292,9 @@ async def setup_before_prep(self, module_test): }, ) + async def setup_after_prep(self, module_test): + await module_test.mock_dns({"evilcorp.com": {"A": ["1.1.1.1"]}}) + def check(self, module_test, events): # Should have queried the IP since in_scope_only=False assert any(e.type == "OPEN_TCP_PORT" and e.data == "1.1.1.1:80" for e in events), ( diff --git a/bbot/test/test_step_2/module_tests/test_module_cloudcheck.py b/bbot/test/test_step_2/module_tests/test_module_cloudcheck.py index 709337476d..92815c2dcc 100644 --- a/bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +++ b/bbot/test/test_step_2/module_tests/test_module_cloudcheck.py @@ -32,22 +32,26 @@ async def setup_after_prep(self, module_test): for event in (ip_event, other_event2): await module.handle_event(ip_event) - assert "cloud-google" in ip_event.tags - assert "google-ip" in ip_event.tags + assert "google" in ip_event.tags + assert "cloud" in ip_event.tags + # check host_metadata has structured info + assert "8.8.8.8" in ip_event.host_metadata + assert "google" in ip_event.host_metadata["8.8.8.8"]["cloud_providers"] + assert ip_event.host_metadata["8.8.8.8"]["cloud_providers"]["google"]["match"] == "ip" for event in (aws_event1, aws_event2, aws_event4, other_event3): await module.handle_event(event) - assert "cloud-amazon" in event.tags, f"{event} was not properly cloud-tagged" + assert "amazon" in event.tags, f"{event} was not properly cloud-tagged" + assert "cloud" in event.tags, f"{event} was not properly cloud-tagged" - assert "amazon-domain" in aws_event1.tags - assert "amazon-cname" in other_event3.tags + # check match types in host_metadata + assert aws_event1.host_metadata["amazonaws.com"]["cloud_providers"]["amazon"]["match"] == "domain" + assert other_event3.host_metadata["asdf.amazonaws.com"]["cloud_providers"]["amazon"]["match"] == "cname" for event in (aws_event3, other_event1): await module.handle_event(event) - assert "cloud-amazon" not in event.tags, f"{event} was improperly cloud-tagged" - assert not any(t for t in event.tags if t.startswith("cloud-") or t.startswith("cdn-")), ( - f"{event} was improperly cloud-tagged" - ) + assert "amazon" not in event.tags, f"{event} was improperly cloud-tagged" + assert "cloud" not in event.tags, f"{event} was improperly cloud-tagged" google_event1 = scan.make_event("asdf.googleapis.com", parent=scan.root_event) google_event2 = scan.make_event("asdf.google", parent=scan.root_event) @@ -56,7 +60,8 @@ async def setup_after_prep(self, module_test): for event in (google_event1, google_event2, google_event3): await module.handle_event(event) - assert "cloud-google" in event.tags, f"{event} was not properly cloud-tagged" + assert "google" in event.tags, f"{event} was not properly cloud-tagged" + assert "cloud" in event.tags, f"{event} was not properly cloud-tagged" await scan._cleanup() @@ -69,8 +74,6 @@ def check(self, module_test, events): if e.type == "STORAGE_BUCKET" and e.data["name"] == "asdf" and str(e.module) == "cloudcheck" - and "cloud-amazon" in e.tags - and "amazon-domain" in e.tags and e.scope_distance == 1 ] ) @@ -81,8 +84,20 @@ def check(self, module_test, events): if e.type == "STORAGE_BUCKET" and e.data["name"] == "asdf2" and str(e.module) == "cloudcheck" - and "cloud-google" in e.tags - and "google-domain" in e.tags and e.scope_distance == 0 ] ) + + # verify host_metadata is populated by cloudcheck on real scan events + url_events = [e for e in events if e.type == "URL"] + for url_event in url_events: + if url_event.host_metadata: + # check structure: {host: {cloud_providers: {name: {types: [...], match: "..."}}}} + for host, meta in url_event.host_metadata.items(): + assert "cloud_providers" in meta, f"host_metadata for {host} missing cloud_providers" + for provider_name, provider_info in meta["cloud_providers"].items(): + assert "types" in provider_info, f"cloud_providers[{provider_name}] missing types" + assert "match" in provider_info, f"cloud_providers[{provider_name}] missing match" + assert provider_info["match"] in ("ip", "domain", "cname"), ( + f"unexpected match type: {provider_info['match']}" + ) diff --git a/bbot/test/test_step_2/module_tests/test_module_csv.py b/bbot/test/test_step_2/module_tests/test_module_csv.py index 5a9575372d..206b9301aa 100644 --- a/bbot/test/test_step_2/module_tests/test_module_csv.py +++ b/bbot/test/test_step_2/module_tests/test_module_csv.py @@ -11,5 +11,5 @@ def check(self, module_test, events): with open(csv_file) as f: data = f.read() - assert "blacklanternsecurity.com,127.0.0.5,TARGET" in data + assert "blacklanternsecurity.com,127.0.0.5,SEED" in data assert context_data in data diff --git a/bbot/test/test_step_2/module_tests/test_module_dehashed.py b/bbot/test/test_step_2/module_tests/test_module_dehashed.py index e566753502..4821fc5458 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dehashed.py +++ b/bbot/test/test_step_2/module_tests/test_module_dehashed.py @@ -8,7 +8,7 @@ class TestDehashed(ModuleTestBase): "modules": {"dehashed": {"api_key": "deadbeef"}}, } - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( url="https://api.dehashed.com/v2/search", method="POST", @@ -119,7 +119,7 @@ def check(self, module_test, events): class TestDehashedHTTPError(TestDehashed): - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( url="https://api.dehashed.com/v2/search", method="POST", diff --git a/bbot/test/test_step_2/module_tests/test_module_discord.py b/bbot/test/test_step_2/module_tests/test_module_discord.py index d1aeb5c60f..96f2d769d9 100644 --- a/bbot/test/test_step_2/module_tests/test_module_discord.py +++ b/bbot/test/test_step_2/module_tests/test_module_discord.py @@ -8,7 +8,7 @@ class TestDiscord(ModuleTestBase): modules_overrides = ["discord", "excavate", "badsecrets", "httpx"] webhook_url = "https://discord.com/api/webhooks/1234/deadbeef-P-uF-asdf" - config_overrides = {"modules": {"discord": {"webhook_url": webhook_url}}} + config_overrides = {"modules": {"discord": {"webhook_url": webhook_url, "min_severity": "INFO"}}} def custom_setup(self, module_test): respond_args = { @@ -34,8 +34,6 @@ def custom_response(request: httpx.Request): module_test.httpx_mock.add_callback(custom_response, url=self.webhook_url) def check(self, module_test, events): - vulns = [e for e in events if e.type == "VULNERABILITY"] findings = [e for e in events if e.type == "FINDING"] - assert len(findings) == 1 - assert len(vulns) == 2 + assert len(findings) == 3 assert module_test.request_count == 4 diff --git a/bbot/test/test_step_2/module_tests/test_module_dnsbimi.py b/bbot/test/test_step_2/module_tests/test_module_dnsbimi.py index c9b9b8757c..5ceb9f44a4 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnsbimi.py @@ -78,7 +78,7 @@ def check(self, module_test, events): # This should not be filtered by a default BBOT configuration assert any( - e.type == "URL_UNVERIFIED" and e.data == "https://bimi.test.localdomain/certificate.pem" for e in events + e.type == "URL_UNVERIFIED" and e.url == "https://bimi.test.localdomain/certificate.pem" for e in events ), "Failed to emit URL_UNVERIFIED" # These should be filtered simply due to distance diff --git a/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py b/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py index 0cbee8440e..a8e1ba750d 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py @@ -95,3 +95,52 @@ def check(self, module_test, events): assert 1 == len( [e for e in events if e.data == "asdf.blacklanternsecurity.com" and str(e.module) == "dnsbrute"] ) + + +class TestDnsbruteCanaryCheck(ModuleTestBase): + """Test that the canary check correctly aborts brute-forcing on wildcard domains. + + Simulates a wildcard domain by making massdns return a result for every input + subdomain, including the canary subdomains. The canary check should detect + this and return no results. + """ + + module_name = "dnsbrute" + subdomain_wordlist = tempwordlist(["www", "mail"]) + config_overrides = {"modules": {"dnsbrute": {"wordlist": str(subdomain_wordlist)}}} + + async def setup_after_prep(self, module_test): + old_run_live = module_test.scan.helpers.run_live + + async def new_run_live(*command, check=False, text=True, **kwargs): + if "massdns" in command[:2]: + # simulate a wildcard domain: return a result for EVERY input subdomain + _input = [l async for l in kwargs["input"]] + for subdomain in _input: + hostname = subdomain.strip() + if hostname: + yield ( + '{"name": "' + + hostname + + '.", "type": "A", "class": "IN", "status": "NOERROR", "data": {"answers": [{"ttl": 86400, "type": "A", "class": "IN", "name": "' + + hostname + + '.", "data": "1.2.3.4"}]}, "resolver": "195.226.187.130:53", "proto": "UDP"}' + ) + else: + async for _ in old_run_live(*command, check=False, text=True, **kwargs): + yield _ + + module_test.monkeypatch.setattr(module_test.scan.helpers, "run_live", new_run_live) + + await module_test.mock_dns( + { + "blacklanternsecurity.com": {"A": ["4.3.2.1"]}, + } + ) + + def check(self, module_test, events): + # canary check should have aborted, so no DNS_NAME events from dnsbrute + dnsbrute_events = [e for e in events if e.type == "DNS_NAME" and str(e.module) == "dnsbrute"] + assert len(dnsbrute_events) == 0, ( + f"Expected no results from dnsbrute (canary check should abort), but got {len(dnsbrute_events)}: {[e.data for e in dnsbrute_events]}" + ) diff --git a/bbot/test/test_step_2/module_tests/test_module_dnscaa.py b/bbot/test/test_step_2/module_tests/test_module_dnscaa.py index cd1546fb1b..8f3a614fe5 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnscaa.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnscaa.py @@ -46,7 +46,7 @@ def check(self, module_test, events): ) assert any(e.type == "DNS_NAME" and e.data == "pki.goog" for e in events), "Failed to detect CA DNS name" assert any( - e.type == "URL_UNVERIFIED" and e.data == "https://caa.blacklanternsecurity.notreal/" for e in events + e.type == "URL_UNVERIFIED" and e.url == "https://caa.blacklanternsecurity.notreal/" for e in events ), "Failed to detect URL" assert any(e.type == "EMAIL_ADDRESS" and e.data == "caa@blacklanternsecurity.notreal" for e in events), ( "Failed to detect email address" diff --git a/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py b/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py index 53c6ff21be..e2b0438b0b 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py @@ -2,8 +2,8 @@ class TestDNSCommonSRV(ModuleTestBase): - targets = ["media.www.test.api.blacklanternsecurity.com"] - whitelist = ["blacklanternsecurity.com"] + seeds = ["media.www.test.api.blacklanternsecurity.com"] + targets = ["blacklanternsecurity.com"] modules_overrides = ["dnscommonsrv", "speculate"] config_overrides = {"dns": {"minimal": False}} diff --git a/bbot/test/test_step_2/module_tests/test_module_dnsresolve.py b/bbot/test/test_step_2/module_tests/test_module_dnsresolve.py index 7940b7aea6..fb7d1b6224 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnsresolve.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnsresolve.py @@ -58,3 +58,68 @@ def check(self, module_test, events): and e.scope_distance == 1 ] ) + + +class TestDNSResolveFilterPTRs(ModuleTestBase): + """Test that PTR-derived hostnames stay as affiliates when filter_ptrs is enabled (default).""" + + module_name = "dnsresolve" + targets = ["192.168.0.1"] + config_overrides = { + "dns": {"minimal": False, "filter_ptrs": True, "search_distance": 1}, + "scope": {"report_distance": 1, "search_distance": 0}, + } + + async def setup_after_prep(self, module_test): + await module_test.mock_dns( + { + "1.0.168.192.in-addr.arpa": {"PTR": ["ptr-host.othercorp.com"]}, + "ptr-host.othercorp.com": {"A": ["192.168.0.1"]}, + } + ) + + def check(self, module_test, events): + # the PTR-derived hostname should have the ptr tag + ptr_events = [e for e in events if e.type == "DNS_NAME" and e.data == "ptr-host.othercorp.com"] + assert len(ptr_events) == 1, f"Expected exactly 1 PTR-derived DNS_NAME, got {len(ptr_events)}" + ptr_event = ptr_events[0] + assert "ptr" in ptr_event.tags, f"PTR-derived event should have 'ptr' tag, has: {ptr_event.tags}" + # it should NOT be promoted to in-scope (scope_distance should stay > 0) + assert ptr_event.scope_distance > 0, ( + f"PTR-derived hostname should not be promoted to in-scope when filter_ptrs=true, " + f"got scope_distance={ptr_event.scope_distance}" + ) + assert "affiliate" in ptr_event.tags, ( + f"PTR-derived hostname should be tagged as affiliate, has: {ptr_event.tags}" + ) + + +class TestDNSResolveFilterPTRsDisabled(ModuleTestBase): + """Test that PTR-derived hostnames ARE promoted to in-scope when filter_ptrs is disabled.""" + + module_name = "dnsresolve" + targets = ["192.168.0.1"] + config_overrides = { + "dns": {"minimal": False, "filter_ptrs": False, "search_distance": 1}, + "scope": {"report_distance": 1, "search_distance": 0}, + } + + async def setup_after_prep(self, module_test): + await module_test.mock_dns( + { + "1.0.168.192.in-addr.arpa": {"PTR": ["ptr-host.othercorp.com"]}, + "ptr-host.othercorp.com": {"A": ["192.168.0.1"]}, + } + ) + + def check(self, module_test, events): + # with filter_ptrs disabled, PTR-derived hostname should be promoted to in-scope + ptr_events = [e for e in events if e.type == "DNS_NAME" and e.data == "ptr-host.othercorp.com"] + assert len(ptr_events) == 1, f"Expected exactly 1 PTR-derived DNS_NAME, got {len(ptr_events)}" + ptr_event = ptr_events[0] + assert "ptr" in ptr_event.tags, f"PTR-derived event should have 'ptr' tag, has: {ptr_event.tags}" + # it SHOULD be promoted to in-scope + assert ptr_event.scope_distance == 0, ( + f"PTR-derived hostname should be promoted to in-scope when filter_ptrs=false, " + f"got scope_distance={ptr_event.scope_distance}" + ) diff --git a/bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py b/bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py index 61900b9189..d33ac11904 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py @@ -46,7 +46,7 @@ def check(self, module_test, events): assert any(e.type == "EMAIL_ADDRESS" and e.data == "test@on.thirdparty.com" for e in events), ( "Failed to detect third party email address" ) - assert any(e.type == "URL_UNVERIFIED" and e.data == "https://tlspost.example.com/" for e in events), ( + assert any(e.type == "URL_UNVERIFIED" and e.url == "https://tlspost.example.com/" for e in events), ( "Failed to detect third party URL" ) diff --git a/bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py b/bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py index 8accc7c300..65835e9492 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py +++ b/bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py @@ -46,7 +46,7 @@ class TestDotnetnuke(ModuleTestBase):
""" - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): # Simulate DotNetNuke Instance expect_args = {"method": "GET", "uri": "/"} respond_args = {"response_data": dotnetnuke_http_response} @@ -92,29 +92,26 @@ def check(self, module_test, events): dnn_installwizard_privesc_detection = False for e in events: - if e.type == "TECHNOLOGY" and "DotNetNuke" in e.data["technology"]: + if e.type == "TECHNOLOGY" and "dotnetnuke" in e.data["technology"]: dnn_technology_detection = True - if ( - e.type == "VULNERABILITY" - and "DotNetNuke Personalization Cookie Deserialization" in e.data["description"] - ): + if e.type == "FINDING" and "DotNetNuke Personalization Cookie Deserialization" in e.data["description"]: dnn_personalization_deserialization_detection = True if ( - e.type == "VULNERABILITY" + e.type == "FINDING" and "DotNetNuke DNNArticle Module GetCSS.ashx Arbitrary File Read" in e.data["description"] ): dnn_getcss_fileread_detection = True if ( - e.type == "VULNERABILITY" + e.type == "FINDING" and "DotNetNuke dnnUI_NewsArticlesSlider Module Arbitrary File Read" in e.data["description"] ): dnn_imagehandler_fileread_detection = True if ( - e.type == "VULNERABILITY" + e.type == "FINDING" and "DotNetNuke InstallWizard SuperUser Privilege Escalation" in e.data["description"] ): dnn_installwizard_privesc_detection = True @@ -137,6 +134,9 @@ class TestDotnetnuke_blindssrf(ModuleTestBase): targets = ["http://127.0.0.1:8888"] module_name = "dotnetnuke" modules_overrides = ["httpx", "dotnetnuke"] + config_overrides = { + "interactsh_disable": False, + } def request_handler(self, request): subdomain_tag = None @@ -147,16 +147,22 @@ def request_handler(self, request): async def setup_before_prep(self, module_test): self.interactsh_mock_instance = module_test.mock_interactsh("dotnetnuke_blindssrf") - module_test.monkeypatch.setattr( - module_test.scan.helpers, "interactsh", lambda *args, **kwargs: self.interactsh_mock_instance - ) - async def setup_after_prep(self, module_test): + # Mock at the helper creation level BEFORE modules are set up + def mock_interactsh_factory(*args, **kwargs): + return self.interactsh_mock_instance + + # Apply the mock to the core helpers so modules get the mock during setup + from bbot.core.helpers.helper import ConfigAwareHelper + + module_test.monkeypatch.setattr(ConfigAwareHelper, "interactsh", mock_interactsh_factory) + # Simulate DotNetNuke Instance expect_args = {"method": "GET", "uri": "/"} respond_args = {"response_data": dotnetnuke_http_response} module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + async def setup_after_prep(self, module_test): expect_args = re.compile("/") module_test.set_expect_requests_handler(expect_args=expect_args, request_handler=self.request_handler) @@ -165,10 +171,10 @@ def check(self, module_test, events): dnn_dnnimagehandler_blindssrf = False for e in events: - if e.type == "TECHNOLOGY" and "DotNetNuke" in e.data["technology"]: + if e.type == "TECHNOLOGY" and "dotnetnuke" in e.data["technology"]: dnn_technology_detection = True - if e.type == "VULNERABILITY" and "DotNetNuke Blind-SSRF (CVE 2017-0929)" in e.data["description"]: + if e.type == "FINDING" and "DotNetNuke Blind-SSRF (CVE 2017-0929)" in e.data["description"]: dnn_dnnimagehandler_blindssrf = True assert dnn_technology_detection, "DNN Technology Detection Failed" diff --git a/bbot/test/test_step_2/module_tests/test_module_elastic.py b/bbot/test/test_step_2/module_tests/test_module_elastic.py new file mode 100644 index 0000000000..9b09890cff --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_elastic.py @@ -0,0 +1,123 @@ +import httpx +import asyncio + +from .base import ModuleTestBase + + +class TestElastic(ModuleTestBase): + config_overrides = { + "modules": { + "elastic": { + "url": "https://localhost:9200/bbot_test_events/_doc", + "username": "elastic", + "password": "bbotislife", + } + } + } + skip_distro_tests = True + + async def setup_before_prep(self, module_test): + # Start Elasticsearch container + await asyncio.create_subprocess_exec( + "docker", + "run", + "--name", + "bbot-test-elastic", + "--rm", + "-e", + "ELASTIC_PASSWORD=bbotislife", + "-e", + "cluster.routing.allocation.disk.watermark.low=96%", + "-e", + "cluster.routing.allocation.disk.watermark.high=97%", + "-e", + "cluster.routing.allocation.disk.watermark.flood_stage=98%", + "-p", + "9200:9200", + "-d", + "docker.elastic.co/elasticsearch/elasticsearch:8.16.0", + ) + + # Connect to Elasticsearch with retry logic + async with httpx.AsyncClient(verify=False) as client: + while True: + try: + # Attempt a simple operation to confirm the connection + response = await client.get("https://localhost:9200/_cat/health", auth=("elastic", "bbotislife")) + response.raise_for_status() + break + except Exception as e: + self.log.verbose(f"Connection failed: {e}. Retrying...") + await asyncio.sleep(0.5) + + # Ensure the index is empty + await client.delete("https://localhost:9200/bbot_test_events", auth=("elastic", "bbotislife")) + + async def check(self, module_test, events): + try: + from bbot.models.pydantic import Event + + events_json = [e.json() for e in events] + events_json.sort(key=lambda x: x["timestamp"]) + + # Connect to Elasticsearch + async with httpx.AsyncClient(verify=False) as client: + # Fetch all events from the index + response = await client.get( + "https://localhost:9200/bbot_test_events/_search?size=100", auth=("elastic", "bbotislife") + ) + response_json = response.json() + db_events = [hit["_source"] for hit in response_json["hits"]["hits"]] + + # make sure we have the same number of events + assert len(events_json) == len(db_events) + + for db_event in db_events: + assert isinstance(db_event["timestamp"], float) + assert isinstance(db_event["inserted_at"], float) + + # Convert to Pydantic objects and dump them + db_events_pydantic = [Event(**e).model_dump(exclude_none=True) for e in db_events] + db_events_pydantic.sort(key=lambda x: x["timestamp"]) + + # Find the main event with type DNS_NAME and data blacklanternsecurity.com + main_event = next( + ( + e + for e in db_events_pydantic + if e.get("type") == "DNS_NAME" and e.get("data") == "blacklanternsecurity.com" + ), + None, + ) + assert main_event is not None, ( + "Main event with type DNS_NAME and data blacklanternsecurity.com not found" + ) + + # Ensure it has the reverse_host attribute + expected_reverse_host = "blacklanternsecurity.com"[::-1] + assert main_event.get("reverse_host") == expected_reverse_host, ( + f"reverse_host attribute is not correct, expected {expected_reverse_host}" + ) + + # Events don't match exactly because the elastic ones have reverse_host and inserted_at + assert events_json != db_events_pydantic + for db_event in db_events_pydantic: + db_event.pop("reverse_host", None) + db_event.pop("inserted_at", None) + db_event.pop("archived", None) + # They should match after removing reverse_host + assert events_json == db_events_pydantic, "Events do not match" + + finally: + # Clean up: Delete all documents in the index + async with httpx.AsyncClient(verify=False) as client: + response = await client.delete( + "https://localhost:9200/bbot_test_events", + auth=("elastic", "bbotislife"), + params={"ignore": "400,404"}, + ) + self.log.verbose("Deleted documents from index") + process = await asyncio.create_subprocess_exec( + "docker", "stop", "bbot-test-elastic", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + await process.communicate() diff --git a/bbot/test/test_step_2/module_tests/test_module_excavate.py b/bbot/test/test_step_2/module_tests/test_module_excavate.py index 085ca0a45d..4fd5854273 100644 --- a/bbot/test/test_step_2/module_tests/test_module_excavate.py +++ b/bbot/test/test_step_2/module_tests/test_module_excavate.py @@ -11,7 +11,7 @@ class TestExcavate(ModuleTestBase): targets = ["http://127.0.0.1:8888/", "test.notreal", "http://127.0.0.1:8888/subdir/links.html"] modules_overrides = ["excavate", "httpx"] - config_overrides = {"web": {"spider_distance": 1, "spider_depth": 1}} + config_overrides = {"web": {"spider_distance": 1, "spider_depth": 1}, "omit_event_types": []} async def setup_before_prep(self, module_test): response_data = """ @@ -64,7 +64,7 @@ async def handle_event(self, event): module_test.scan.modules["dummy_module"] = DummyModule(module_test.scan) def check(self, module_test, events): - event_data = [e.data for e in events] + event_data = [e.pretty_string for e in events] assert "https://www1.test.notreal/" in event_data assert "https://www2.test.notreal/" in event_data assert "https://www3.test.notreal/" in event_data @@ -83,7 +83,7 @@ def check(self, module_test, events): assert "http://127.0.0.1:8888/link_relative.js" not in event_data assert "http://127.0.0.1:8888/a_relative.txt" in event_data assert "http://127.0.0.1:8888/link_relative.txt" in event_data - dummy_module_event_data = [e.data for e in module_test.scan.modules["dummy_module"].events_seen] + dummy_module_event_data = [e.pretty_string for e in module_test.scan.modules["dummy_module"].events_seen] assert "http://127.0.0.1:8888/a_relative.js" in dummy_module_event_data assert "http://127.0.0.1:8888/link_relative.js" in dummy_module_event_data assert "http://127.0.0.1:8888/a_relative.txt" in dummy_module_event_data @@ -107,7 +107,7 @@ def check(self, module_test, events): assert any( e.type == "URL_UNVERIFIED" - and e.data == "http://127.0.0.1:8888/relative.html" + and e.url == "http://127.0.0.1:8888/relative.html" and "spider-max" not in e.tags and "endpoint" in e.tags and "extension-html" in e.tags @@ -117,17 +117,17 @@ def check(self, module_test, events): ) assert any( - e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/2/depth2.html" and "spider-max" in e.tags + e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/2/depth2.html" and "spider-max" in e.tags for e in events ) assert any( - e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/distance2.html" and "spider-max" in e.tags + e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/distance2.html" and "spider-max" in e.tags for e in events ) assert any( - e.type == "URL_UNVERIFIED" and "miscellaneous.html" in e.data and "x50-uart-driver" not in e.data + e.type == "URL_UNVERIFIED" and "miscellaneous.html" in e.url and "x50-uart-driver" not in e.url for e in events ) @@ -168,17 +168,17 @@ def check(self, module_test, events): for e in events: if e.type == "URL_UNVERIFIED": # these cases represent the desired behavior for parsing relative links - if e.data == "http://127.0.0.1:8888/rootrelative.html": + if e.url == "http://127.0.0.1:8888/rootrelative.html": root_relative_detection = True - if e.data == "http://127.0.0.1:8888/subdir/pagerelative1.html": + if e.url == "http://127.0.0.1:8888/subdir/pagerelative1.html": page_relative_detection_1 = True - if e.data == "http://127.0.0.1:8888/subdir/pagerelative2.html": + if e.url == "http://127.0.0.1:8888/subdir/pagerelative2.html": page_relative_detection_2 = True # these cases indicates that excavate parsed the relative links incorrectly - if e.data == "http://127.0.0.1:8888/pagerelative.html": + if e.url == "http://127.0.0.1:8888/pagerelative.html": root_page_confusion_1 = True - if e.data == "http://127.0.0.1:8888/subdir/rootrelative.html": + if e.url == "http://127.0.0.1:8888/subdir/rootrelative.html": root_page_confusion_2 = True assert root_relative_detection, "Failed to properly excavate root-relative URL" @@ -202,7 +202,7 @@ async def setup_before_prep(self, module_test): def check(self, module_test, events): found_js_url_event = bool( - [e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/script.js"] + [e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/script.js"] ) found_excavate_jwt_finding = bool( [ @@ -211,18 +211,16 @@ def check(self, module_test, events): if e.type == "FINDING" and "JWT" in e.data["description"] and str(e.module) == "excavate" ] ) - found_badsecrets_vulnerability = bool( - [e for e in events if e.type == "VULNERABILITY" and str(e.module) == "badsecrets"] - ) + found_badsecrets_finding = bool([e for e in events if e.type == "FINDING" and str(e.module) == "badsecrets"]) assert found_js_url_event, "Failed to find URL event for script.js" - assert found_badsecrets_vulnerability, "Failed to find BADSECRETs event from script.js" - assert found_excavate_jwt_finding, "Failed to find JWT finding from script.js" + assert found_badsecrets_finding, "Failed to find BADSECRETs finding from script.js" + assert found_excavate_jwt_finding, "Excavate should still emit JWT findings even when badsecrets is enabled" class TestExcavateRedirect(TestExcavate): targets = ["http://127.0.0.1:8888/", "http://127.0.0.1:8888/relative/", "http://127.0.0.1:8888/nonhttpredirect/"] - config_overrides = {"scope": {"report_distance": 1}} + config_overrides = {"scope": {"report_distance": 1}, "omit_event_types": []} async def setup_before_prep(self, module_test): # absolute redirect @@ -245,10 +243,10 @@ def check(self, module_test, events): [ e for e in events - if e.type == "URL_UNVERIFIED" and e.data == "https://www.test.notreal/yep" and e.scope_distance == 1 + if e.type == "URL_UNVERIFIED" and e.url == "https://www.test.notreal/yep" and e.scope_distance == 1 ] ) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/relative/owa/"]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/relative/owa/"]) assert 1 == len( [ e @@ -289,7 +287,7 @@ def check(self, module_test, events): class TestExcavateQuerystringRemoveTrue(TestExcavate): targets = ["http://127.0.0.1:8888/"] - config_overrides = {"url_querystring_remove": True, "url_querystring_collapse": True} + config_overrides = {"url_querystring_remove": True, "url_querystring_collapse": True, "omit_event_types": []} lots_of_params = """ @@ -309,12 +307,12 @@ async def setup_before_prep(self, module_test): def check(self, module_test, events): assert len([e for e in events if e.type == "URL_UNVERIFIED"]) == 2 assert ( - len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/endpoint"]) == 1 + len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/endpoint"]) == 1 ) class TestExcavateQuerystringRemoveFalse(TestExcavateQuerystringRemoveTrue): - config_overrides = {"url_querystring_remove": False, "url_querystring_collapse": True} + config_overrides = {"url_querystring_remove": False, "url_querystring_collapse": True, "omit_event_types": []} def check(self, module_test, events): assert ( @@ -322,7 +320,7 @@ def check(self, module_test, events): [ e for e in events - if e.type == "URL_UNVERIFIED" and e.data.startswith("http://127.0.0.1:8888/endpoint?") + if e.type == "URL_UNVERIFIED" and e.url.startswith("http://127.0.0.1:8888/endpoint?") ] ) == 1 @@ -330,7 +328,7 @@ def check(self, module_test, events): class TestExcavateQuerystringCollapseFalse(TestExcavateQuerystringRemoveTrue): - config_overrides = {"url_querystring_remove": False, "url_querystring_collapse": False} + config_overrides = {"url_querystring_remove": False, "url_querystring_collapse": False, "omit_event_types": []} def check(self, module_test, events): assert ( @@ -338,7 +336,7 @@ def check(self, module_test, events): [ e for e in events - if e.type == "URL_UNVERIFIED" and e.data.startswith("http://127.0.0.1:8888/endpoint?") + if e.type == "URL_UNVERIFIED" and e.url.startswith("http://127.0.0.1:8888/endpoint?") ] ) == 10 @@ -347,7 +345,7 @@ def check(self, module_test, events): class TestExcavateMaxLinksPerPage(TestExcavate): targets = ["http://127.0.0.1:8888/"] - config_overrides = {"web": {"spider_links_per_page": 10, "spider_distance": 1}} + config_overrides = {"web": {"spider_links_per_page": 10, "spider_distance": 1}, "omit_event_types": []} lots_of_links = """ @@ -384,7 +382,9 @@ def check(self, module_test, events): url_unverified_events = [e for e in events if e.type == "URL_UNVERIFIED"] # base URL + 25 links = 26 assert len(url_unverified_events) == 26 - url_data = [e.data for e in url_unverified_events if "spider-max" not in e.tags and "spider-danger" in e.tags] + url_data = [ + e.pretty_string for e in url_unverified_events if "spider-max" not in e.tags and "spider-danger" in e.tags + ] assert len(url_data) >= 10 and len(url_data) <= 12 url_events = [e for e in events if e.type == "URL"] assert len(url_events) == 11 @@ -410,7 +410,7 @@ async def setup_before_prep(self, module_test): def check(self, module_test, events): assert any(e.data == "asdffoo.test.notreal" for e in events) - assert any(e.data == "https://asdffoo.test.notreal/some/path" for e in events) + assert any(e.url == "https://asdffoo.test.notreal/some/path" for e in events) class TestExcavateURL_IP(TestExcavate): @@ -421,7 +421,7 @@ async def setup_before_prep(self, module_test): def check(self, module_test, events): assert any(e.data == "127.0.0.2" for e in events) - assert any(e.data == "https://127.0.0.2/some/path" for e in events) + assert any(e.url == "https://127.0.0.2/some/path" for e in events) class TestExcavateSerializationNegative(TestExcavate): @@ -473,6 +473,8 @@ class TestExcavateNonHttpScheme(TestExcavate):

hxxp://test.notreal

ftp://test.notreal

nonsense://test.notreal

+

ws://test.notreal

+

wss://test.notreal

""" @@ -484,6 +486,9 @@ def check(self, module_test, events): found_hxxp_url = False found_ftp_url = False found_nonsense_url = False + found_ws_finding = False + found_ws_url = False + found_wss_url = False for e in events: if e.type == "FINDING": @@ -493,9 +498,19 @@ def check(self, module_test, events): found_ftp_url = True if "nonsense" in e.data["description"]: found_nonsense_url = True + if "ws://" in e.data.get("description", "") or "wss://" in e.data.get("description", ""): + found_ws_finding = True + if e.type == "URL_UNVERIFIED": + if e.data.get("url", "") == "http://test.notreal/": + found_ws_url = True + if e.data.get("url", "") == "https://test.notreal/": + found_wss_url = True assert found_hxxp_url assert found_ftp_url assert not found_nonsense_url + assert not found_ws_finding, "ws:// should not produce a FINDING" + assert found_ws_url, "ws:// should be converted to http:// URL_UNVERIFIED" + assert found_wss_url, "wss:// should be converted to https:// URL_UNVERIFIED" class TestExcavateParameterExtraction(TestExcavate): @@ -1088,6 +1103,56 @@ class TestExcavateYaraCustom(TestExcavateYara): config_overrides = {"modules": {"excavate": {"custom_yara_rules": f}}} +class TestExcavateYaraConfidence(ModuleTestBase): + """Test YARA rules with confidence options.""" + + targets = ["http://127.0.0.1:8888/"] + modules_overrides = ["excavate", "httpx"] + + async def setup_before_prep(self, module_test): + yara_test_html = """ + +

CONFIRMED_SECRET_DATA

+

HIGH_CONFIDENCE_INDICATOR

+

MODERATE_RISK_PATTERN

+

LOW_CONFIDENCE_MATCH

+

UNKNOWN_PATTERN_TYPE

+ + """ + module_test.httpserver.expect_request("/").respond_with_data(yara_test_html) + + async def setup_after_prep(self, module_test): + excavate_module = module_test.scan.modules["excavate"] + excavateruleinstance = excavateTestRule(excavate_module) + + # Add YARA rules with different confidence levels + yara_rules = { + "ConfirmedRule": 'rule ConfirmedRule { meta: description = "Confirmed rule" severity = "HIGH" confidence = "CONFIRMED" strings: $text = "CONFIRMED_SECRET_DATA" condition: $text }', + "HighConfidenceRule": 'rule HighConfidenceRule { meta: description = "High confidence rule" severity = "MEDIUM" confidence = "HIGH" strings: $text = "HIGH_CONFIDENCE_INDICATOR" condition: $text }', + "ModerateConfidenceRule": 'rule ModerateConfidenceRule { meta: description = "Moderate confidence rule" severity = "LOW" confidence = "MEDIUM" strings: $text = "MODERATE_RISK_PATTERN" condition: $text }', + "LowConfidenceRule": 'rule LowConfidenceRule { meta: description = "Low confidence rule" severity = "INFO" confidence = "LOW" strings: $text = "LOW_CONFIDENCE_MATCH" condition: $text }', + "UnknownConfidenceRule": 'rule UnknownConfidenceRule { meta: description = "Unknown confidence rule" severity = "INFO" confidence = "UNKNOWN" strings: $text = "UNKNOWN_PATTERN_TYPE" condition: $text }', + } + + for rule_name, rule_content in yara_rules.items(): + excavate_module.add_yara_rule(rule_name, rule_content, excavateruleinstance) + + excavate_module.yara_rules = yara.compile(source="\n".join(excavate_module.yara_rules_dict.values())) + + def check(self, module_test, events): + """Verify findings are created with correct confidence levels.""" + findings = [e for e in events if e.type == "FINDING"] + confidence_findings = {f.data.get("confidence", "UNKNOWN"): f for f in findings} + + # Verify all confidence levels are present + expected_confidences = ["CONFIRMED", "HIGH", "MEDIUM", "LOW", "UNKNOWN"] + for confidence in expected_confidences: + assert confidence in confidence_findings, f"Missing finding with confidence: {confidence}" + finding = confidence_findings[confidence] + assert finding.data["confidence"] == confidence + assert f"confidence-{confidence.lower()}" in finding.tags + + class TestExcavateSpiderDedupe(ModuleTestBase): class DummyModule(BaseModule): watched_events = ["URL_UNVERIFIED"] @@ -1097,7 +1162,7 @@ class DummyModule(BaseModule): async def handle_event(self, event): await self.helpers.sleep(0.5) - self.events_seen.append(event.data) + self.events_seen.append(event.url) new_event = self.scan.make_event(event.data, "URL_UNVERIFIED", self.scan.root_event) if new_event is not None: await self.emit_event(new_event) @@ -1105,6 +1170,7 @@ async def handle_event(self, event): dummy_text = "
spider" modules_overrides = ["excavate", "httpx"] targets = ["http://127.0.0.1:8888/"] + config_overrides = {"omit_event_types": []} async def setup_after_prep(self, module_test): self.dummy_module = self.DummyModule(module_test.scan) @@ -1121,7 +1187,7 @@ def check(self, module_test, events): for e in events: if e.type == "URL_UNVERIFIED": - if e.data == "http://127.0.0.1:8888/spider": + if e.url == "http://127.0.0.1:8888/spider": if str(e.module) == "excavate" and "spider-danger" in e.tags and "spider-max" in e.tags: found_url_unverified_spider_max = True if ( @@ -1130,7 +1196,7 @@ def check(self, module_test, events): and "spider-max" not in e.tags ): found_url_unverified_dummy = True - if e.type == "URL" and e.data == "http://127.0.0.1:8888/spider": + if e.type == "URL" and e.url == "http://127.0.0.1:8888/spider": found_url_event = True assert found_url_unverified_spider_max, "Excavate failed to find /spider link" @@ -1273,13 +1339,14 @@ def check(self, module_test, events): class TestExcavateRAWTEXT(ModuleTestBase): targets = ["http://127.0.0.1:8888/", "test.notreal"] - modules_overrides = ["excavate", "httpx", "filedownload", "extractous"] + modules_overrides = ["excavate", "httpx", "filedownload", "kreuzberg"] config_overrides = { "scope": {"report_distance": 1}, "web": {"spider_distance": 2, "spider_depth": 2}, "modules": { "filedownload": {"output_folder": str(bbot_test_dir / "filedownload")}, }, + "omit_event_types": [], } pdf_data = r"""%PDF-1.3 @@ -1350,15 +1417,7 @@ class TestExcavateRAWTEXT(ModuleTestBase): startxref 1669 %%EOF""" - extractous_response = """This is an email example@blacklanternsecurity.notreal - -An example JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c - -A serialized DOTNET object AAEAAAD/////AQAAAAAAAAAMAgAAAFJTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5MaXN0YDFbW1N5c3RlbS5TdHJpbmddXSwgU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49YjAzZjVmN2YxMWQ1MGFlMwEAAAAIQ29tcGFyZXIQSXRlbUNvdW50AQMAAAAJAwAAAAlTeXN0ZW0uU3RyaW5nW10FAAAACQIAAAAJBAAAAAkFAAAACRcAAAAJCgAAAAkLAAAACQwAAAAJDQAAAAkOAAAACQ8AAAAJEAAAAAkRAAAACRIAAAAJEwAAAA== - -A full url https://www.test.notreal/about - -A href Click me""" + kreuzberg_response = "This is an email example@blacklanternsecurity.notreal An example JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c A serialized DOTNET object AAEAAAD/////AQAAAAAAAAAMAgAAAFJTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5MaXN0YDFbW1N5c3RlbS5TdHJpbmddXSwgU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49YjAzZjVmN2YxMWQ1MGFlMwEAAAAIQ29tcGFyZXIQSXRlbUNvdW50AQMAAAAJAwAAAAlTeXN0ZW0uU3RyaW5nW10FAAAACQIAAAAJBAAAAAkFAAAACRcAAAAJCgAAAAkLAAAACQwAAAAJDQAAAAkOAAAACQ8AAAAJEAAAAAkRAAAACRIAAAAJEwAAAA== A full url https://www.test.notreal/about A href Click me" async def setup_after_prep(self, module_test): module_test.set_expect_requests( @@ -1379,13 +1438,13 @@ def check(self, module_test, events): assert open(file).read() == self.pdf_data, f"File at {file} does not contain the correct content" raw_text_events = [e for e in events if e.type == "RAW_TEXT"] assert 1 == len(raw_text_events), "Failed to emit RAW_TEXT event" - assert raw_text_events[0].data == self.extractous_response, ( + assert raw_text_events[0].data == self.kreuzberg_response, ( f"Text extracted from PDF is incorrect, got {raw_text_events[0].data}" ) email_events = [e for e in events if e.type == "EMAIL_ADDRESS"] assert 1 == len(email_events), "Failed to emit EMAIL_ADDRESS event" assert email_events[0].data == "example@blacklanternsecurity.notreal", ( - f"Email extracted from extractous text is incorrect, got {email_events[0].data}" + f"Email extracted from kreuzberg text is incorrect, got {email_events[0].data}" ) finding_events = [e for e in events if e.type == "FINDING"] assert 2 == len(finding_events), "Failed to emit FINDING events" @@ -1408,12 +1467,12 @@ def check(self, module_test, events): for e in finding_events ), f"Failed to emit serialized event got {finding_events}" assert finding_events[0].data["path"] == str(file), "File path not included in finding event" - url_events = [e.data for e in events if e.type == "URL_UNVERIFIED"] + url_events = [e.pretty_string for e in events if e.type == "URL_UNVERIFIED"] assert "https://www.test.notreal/about" in url_events, ( - f"URL extracted from extractous text is incorrect, got {url_events}" + f"URL extracted from kreuzberg text is incorrect, got {url_events}" ) assert "/donot_detect.js" not in url_events, ( - f"URL extracted from extractous text is incorrect, got {url_events}" + f"URL extracted from kreuzberg text is incorrect, got {url_events}" ) @@ -1457,7 +1516,7 @@ def check(self, module_test, events): class TestExcavateBadURLs(ModuleTestBase): targets = ["http://127.0.0.1:8888/"] modules_overrides = ["excavate", "httpx", "hunt"] - config_overrides = {"interactsh_disable": True, "scope": {"report_distance": 10}} + config_overrides = {"interactsh_disable": True, "scope": {"report_distance": 10}, "omit_event_types": []} bad_url_data = """ Help @@ -1468,15 +1527,22 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests({"uri": "/"}, {"response_data": self.bad_url_data}) def check(self, module_test, events): + import gzip + debug_log_content = open(module_test.scan.home / "debug.log").read() + for archived_debug_log in module_test.scan.home.glob("debug.log.*.gz"): + gzipped_content = open(archived_debug_log).read() + ungzipped_content = gzip.decompress(gzipped_content).decode("utf-8") + debug_log_content += ungzipped_content + # make sure our logging is working - assert "Setting scan status to STARTING" in debug_log_content + assert "Setting scan status to RUNNING" in debug_log_content # make sure we don't have any URL validation errors assert "Error Parsing reconstructed URL" not in debug_log_content assert "Error sanitizing event data" not in debug_log_content url_events = [e for e in events if e.type == "URL_UNVERIFIED"] - assert sorted([e.data for e in url_events]) == sorted(["https://ssl/", "http://127.0.0.1:8888/"]) + assert sorted([e.pretty_string for e in url_events]) == sorted(["https://ssl/", "http://127.0.0.1:8888/"]) class TestExcavateURL_InvalidPort(TestExcavate): @@ -1509,7 +1575,7 @@ async def setup_after_prep(self, module_test): def check(self, module_test, events): # excavate should skip PDF responses entirely, so no URLs or findings should be extracted from the body url_unverified_events = [ - e for e in events if e.type == "URL_UNVERIFIED" and "pdf-extracted.test.notreal" in e.data + e for e in events if e.type == "URL_UNVERIFIED" and "pdf-extracted.test.notreal" in e.url ] assert len(url_unverified_events) == 0, ( f"PDF body should not be processed by excavate, but got: {url_unverified_events}" diff --git a/bbot/test/test_step_2/module_tests/test_module_extractous.py b/bbot/test/test_step_2/module_tests/test_module_extractous.py deleted file mode 100644 index 19380dbf06..0000000000 --- a/bbot/test/test_step_2/module_tests/test_module_extractous.py +++ /dev/null @@ -1,66 +0,0 @@ -import base64 -from pathlib import Path -from .base import ModuleTestBase - -from ...bbot_fixtures import * - - -class TestExtractous(ModuleTestBase): - targets = ["http://127.0.0.1:8888"] - modules_overrides = ["extractous", "filedownload", "httpx", "excavate", "speculate"] - config_overrides = { - "web": { - "spider_distance": 2, - "spider_depth": 2, - }, - "modules": { - "filedownload": { - "output_folder": bbot_test_dir / "filedownload", - }, - }, - } - - pdf_data = base64.b64decode( - "JVBERi0xLjMKJe+/ve+/ve+/ve+/vSBSZXBvcnRMYWIgR2VuZXJhdGVkIFBERiBkb2N1bWVudCBodHRwOi8vd3d3LnJlcG9ydGxhYi5jb20KMSAwIG9iago8PAovRjEgMiAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL0Jhc2VGb250IC9IZWx2ZXRpY2EgL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcgL05hbWUgL0YxIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAvRm9udAo+PgplbmRvYmoKMyAwIG9iago8PAovQ29udGVudHMgNyAwIFIgL01lZGlhQm94IFsgMCAwIDU5NS4yNzU2IDg0MS44ODk4IF0gL1BhcmVudCA2IDAgUiAvUmVzb3VyY2VzIDw8Ci9Gb250IDEgMCBSIC9Qcm9jU2V0IFsgL1BERiAvVGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSSBdCj4+IC9Sb3RhdGUgMCAvVHJhbnMgPDwKCj4+IAogIC9UeXBlIC9QYWdlCj4+CmVuZG9iago0IDAgb2JqCjw8Ci9QYWdlTW9kZSAvVXNlTm9uZSAvUGFnZXMgNiAwIFIgL1R5cGUgL0NhdGFsb2cKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL0F1dGhvciAoYW5vbnltb3VzKSAvQ3JlYXRpb25EYXRlIChEOjIwMjQwNjAzMTg1ODE2KzAwJzAwJykgL0NyZWF0b3IgKFJlcG9ydExhYiBQREYgTGlicmFyeSAtIHd3dy5yZXBvcnRsYWIuY29tKSAvS2V5d29yZHMgKCkgL01vZERhdGUgKEQ6MjAyNDA2MDMxODU4MTYrMDAnMDAnKSAvUHJvZHVjZXIgKFJlcG9ydExhYiBQREYgTGlicmFyeSAtIHd3dy5yZXBvcnRsYWIuY29tKSAKICAvU3ViamVjdCAodW5zcGVjaWZpZWQpIC9UaXRsZSAodW50aXRsZWQpIC9UcmFwcGVkIC9GYWxzZQo+PgplbmRvYmoKNiAwIG9iago8PAovQ291bnQgMSAvS2lkcyBbIDMgMCBSIF0gL1R5cGUgL1BhZ2VzCj4+CmVuZG9iago3IDAgb2JqCjw8Ci9GaWx0ZXIgWyAvQVNDSUk4NURlY29kZSAvRmxhdGVEZWNvZGUgXSAvTGVuZ3RoIDEwNwo+PgpzdHJlYW0KR2FwUWgwRT1GLDBVXEgzVFxwTllUXlFLaz90Yz5JUCw7VyNVMV4yM2loUEVNXz9DVzRLSVNpOTBNakdeMixGUyM8UkM1K2MsbilaOyRiSyRiIjVJWzwhXlREI2dpXSY9NVgsWzVAWUBWfj5lbmRzdHJlYW0KZW5kb2JqCnhyZWYKMCA4CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDA3MyAwMDAwMCBuIAowMDAwMDAwMTA0IDAwMDAwIG4gCjAwMDAwMDAyMTEgMDAwMDAgbiAKMDAwMDAwMDQxNCAwMDAwMCBuIAowMDAwMDAwNDgyIDAwMDAwIG4gCjAwMDAwMDA3NzggMDAwMDAgbiAKMDAwMDAwMDgzNyAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9JRCAKWzw4MGQ5ZjViOTY0ZmM5OTI4NDUwMWRlYjdhNmE2MzdmNz48ODBkOWY1Yjk2NGZjOTkyODQ1MDFkZWI3YTZhNjM3Zjc+XQolIFJlcG9ydExhYiBnZW5lcmF0ZWQgUERGIGRvY3VtZW50IC0tIGRpZ2VzdCAoaHR0cDovL3d3dy5yZXBvcnRsYWIuY29tKQoKL0luZm8gNSAwIFIKL1Jvb3QgNCAwIFIKL1NpemUgOAo+PgpzdGFydHhyZWYKMTAzNAolJUVPRg==" - ) - - docx_data = base64.b64decode( - "UEsDBBQAAAAIAK+YSUqNEzDqOgEAAKcCAAAQAAAAZG9jUHJvcHMvYXBwLnhtbK2SzU4CMRSFX6Xp3ungghjCDDGwcKHGBMR1be8wjf1Le0Hm2Vz4SL6C7SAM6s7YXc/5eu5P+vH2Pp3tjSY7CFE5W9FRUVICVjip7KaiW2wuriiJyK3k2lmoaAeRzuop95OH4DwEVBBJyrCxoi2inzAWRQuGxyLZNjmNC4ZjuoYNc02jBCyc2BqwyC7Lcsxgj2AlyAt/CqSHxMkO/xoqncj9xfWq80Me9//ZZL+Fa++1EhzT+uo7JYKLrkHy5IIkKZNgC+QVnqfsB5qfpgpLENugsKvLnjhXMrEUXMM8VawbriP0zKBlYu6M57Yj7MC3PIBMKef8ScvETVpH0Mq+xHnL7QbkGfnb+xpwffge9WhclOkchznKmVqB8Zoj1Pd5kbqQDk3PnYxM3ebwR79yi6wMlb/rvTT8rvoTUEsDBBQAAAAIAAVuZllQ34JjWAEAAIwCAAARAAAAZG9jUHJvcHMvY29yZS54bWyNUt1OgzAUfhXSeyiFDWcDLFHjhXGJiUs03tVytuGAkvZMtmfzwkfyFey6wZzxQq4O5/tpvw++Pj7T6bauvHfQplRNRlgQEg8aqYqyWWZkgwt/QqZ5KpWGB61a0FiC8aymMbyQGVkhtpzSdqOrQOklLSSFCmpo0FAWMEoGLoKuzZ8ChwzMrSkHVtd1QRc7XhSGjD7P7h/lCmrhl41B0Ug4qgaFcbAJ7FUbiyyUrgUa59AKuRZL2DsltAYUhUBB98n8dohG8rSQHEusIE/t5frRTmbz+gYSD+vhZQ27TunC2PVptIQCjNRli7bWg+JscayDSw0CofBsaI67FjLSI0/x9c38luRRGI18xvwwmUeMjyac2VrjywsWxi973zOfk3Ftv+Ci/LdzxFnCx+Pg0j7jMPnh3Bu5UO4YpfM7BZU3U7Y6F61fp5UwODsKrnZntF9Q6oo//VL5N1BLAwQUAAAACAAFbmZZnlZ1fjgCAAACCQAAEgAAAHdvcmQvZm9udFRhYmxlLnhtbOWV0W7aMBRAf8Xye4mTAKWoaUWhSJOmPUz7AeM4xFpsR76GwLftYZ+0X9hNSAAVoTaVxsuClIR7fY/t44v48+v34/NOF2QrHShrEhoOGCXSCJsqs07oxmd3E0rAc5PywhqZ0L0E+vz0WE0zazwQrDYw1SKhufflNAhA5FJzGNhSGkxm1mnu8atbB5q7n5vyTlhdcq9WqlB+H0SMjWmLcR+h2CxTQi6s2GhpfFMfOFkg0RrIVQkdrfoIrbIuLZ0VEgB3rIsDT3NljphweAHSSjgLNvMD3Ey7ogaF5SFr3nRxAoz6AaK3gPJzSzjta+F4hY/TisYg+xFH7ZoC2Gu5OwMJlfYjjTsSVp5x+kEmF47HQu4+xwiw8txM6tO8FynqTjyoa7nnOYecEi2mX9bGOr4qUDa2EcFOIPVhkuYA6js6qB/Nq9yRbnra/cBINTVcY/mcF2rlVJMoubEgQ8xteZFQnH/JRnivP0MW13dKgnqkyLkD6Y8jWRvPuFbFvgtDpQDaTKm8yLvEljtVr77NgVpjZgMrltBXhle0XNJDJEzoEAOz+TES1dM1V9hG4mMEl4FraziHEQ/LNhKej8FJg4OGCx0/lJZAvsmKfLeamytaIjZGHSOUUuuJe2pxDbm/lmh2rmWOkfvJML7Q8vC+lmVfLW2XkK9qnfurvRLfuFdmTa+8vumViN2/XEhh/6BXZqW3QBYKyoLvr0h5Qciw1RLdRAr+z6CDyf1JSruX+HZS/lsZ7Qs8/QVQSwMEFAAAAAgABW5mWcFNiUYeBAAADgwAABEAAAB3b3JkL3NldHRpbmdzLnhtbLVWXW7bOBC+iqDndWxJtpMITQv/xNsW8XYRZw9AiSObCH8EkrLjFnuyfdgj7RV2KImRnRhBkqIvNjnfzDcz5HBG//3z74dPD4IHW9CGKXkVRmeDMACZK8rk+iqsbNG7CANjiaSEKwlX4R5M+Onjh11qwFpUMgESSJOK/CrcWFum/b7JNyCIOVMlSAQLpQWxuNXrviD6vip7uRIlsSxjnNl9Px4MxmFLo9CplmlL0RMs18qowjqTVBUFy6H98xb6NX4bk7nKKwHS1h77GjjGoKTZsNJ4NvFeNgQ3nmT7UhJbwb3eLhq8It2d0vTR4jXhOYNSqxyMwQsS3AfIZOd4+Izo0fcZ+m5TrKnQPBrUq8PIR28jiJ8QGP6aTBrohmWa6P2JNMr35dEdzlyTHf51aY0NvI1x1CbWN3sBDwdEOaNvYxp7JrQ84HkbycWzixrn8PA+jj5aHp4MtXTzJqbYl03f2RJLNsTgIxF5+mUtlSYZx8PGWgywnAJ3mUF9Ae4Xz8D91Ut4CLz70HWe70qJYJeWoHN8fti0Bti0+g6xmuT3t7BlrpsZ1NkSrLOCcAOtBoWCVNzekWxlVek1zmPPkG8IcljQq5LkWBkzJa1W3CtS9YeyM+xeGuvHm9TNrFutmsaIJpIITPCo2S0VxVB2aaXZ648y9O6j0ZHPp54U9nHNKGB2HFZ2z2GB4a/Yd5hI+rUyliFl3fN+IoQXIwDpXH/Dl3y3L2EBxFZ4Ur/KW30bC87KJdNa6S+SYj38Om+sKECjB0YsLLGImFa7+qg/A6E4QX/Wcf+wlnAgU+MXt0pZr5sk0/PRYHzRxupgDw0W15NJMr8+Ab1gNVwk04tZkpyAxuPr6XQ6n52Azi/iZLaYDH3kbbwidYPwT+1XrgAD0ZjMiMg0I8GyHpVoJtJM30+Z9AoZYPOHI2hVZR7t9VrECML5Ap+pR5rHK1LKTDmHotnwJdHrjtvr6NNi7AtfH/lcWwH9u1ZV2cI7TcqmvLxONBx6WybtDRMeMFW2erSTOLcOsErSb1vdHFl3Uti3sFDqt3pD6oKrlUH2/lr5iuR65aoJlqQsm6LM1tFVyNl6Y7F6kAJ3FL+u6k22jlssrrG4weoNyV16qN0uOhlqtYtOlnhZ0slw5raLToafA+2ik+HoahdOtsGGoDmT9/g+/NLJC8W52gH93OHPRO0pmA0pYd50cKw11Qjalm6CbQoPOA6AMovfrCWjguDkiwbxuLZv1TnZq8oeKTvMaZfHFG5iPb7NI+u64p9E42ZLzrA0V3uRdRPjrI2dM4MdpcTpYpX24G8NGA1TqvIvbuINTz3XaFTPJXvnxhve/i0UU2KAetAbjxrjH5NoMru8XiS9y+Q87g3Pk6h3OR7Pe1izl4vF5SCeRbO//cP13/Ef/wdQSwMEFAAAAAgABW5mWX+NfPRJDgAA56IAAA8AAAB3b3JkL3N0eWxlcy54bWztXdt227gV/RUuPbUPHlm8Scoaz6zESZpMc/HEns4zREIWY4pUeYnt/lof+kn9hQIgSFEiIZPEtuzMdGWtWLxgAzz77IMrif/++z8//ny3Do1vNEmDODobTX44HRk08mI/iK7PRnm2PJmNjDQjkU/COKJno3uajn7+6cfbF2l2H9LUYMmj9MXtZmKfjVZZtnkxHqfeiq5J+sM68JI4jZfZD168HsfLZeDR8W2c+GPzdHIqfm2S2KNpyjJ7nZBb9mckAddeAy7e0IhdXMbJmmTsMLker0lyk29OGPyGZMEiCIPsnoGfuiVM0gWlKNnr2MvXNMpE+nFCQ4YYR+kq2KQl2m0XtN3HWocF3poEUQUzzFbrcAvg9AMwGwBuSvtBOBJinN6v6V0NyAv8fkhuicRS1nD6gcyaT+TRu2EYY5aybhk/81e9kMySoDFPSzKyIulqZKy9F++vozghi5AZm7FuMOIMLhVDEMD/Zzbgf8RPemeU2Y+4wvzYe02XJA+zlB8mF4k8lEfiz9s4ylLj9gVJvSC4YmVlWa0Dluu7l1EajNiVFf/ReoWSNHuZBqR+8Y08x697aVa78irwWaqx0P6/2NVvJDwbmXZ16jxtnAxJdF2epNHJb5f1XM9GX8nJLxf81IJBn41IcnL5UqQcy+cb7z/1Zv9IZL0hHpMaN8Iyo0zyE5dFMZZ7wAOWOZ2XB19yTgTJs7jMRSCMd3HHDcuzUMACw2UR8NhVuvwQezfUv8zYhbORyIyd/O39RRLECYtBZ6P5XJ68pOvgXeD7NKrdGK0Cn/6+otFvKfW35399K+KIPOHFecR+W9OJ8IYw9d/ceXTDoxK7GhFOzCeegIng9kUebDMXyf9Zgk1KMtoAVpTwUG9M9jHm/THMVoy0ZoAil72nn/TPyTpaTsyTj5STc7ScWO14pJymR8uJtVKOlNP80XMKIp9VBZOusA8BCVkigITqEEBCVAggoRkEkJAEAkh4PAJIODQCaK4PlMVes4KwQMCNWgMF3KgkUMCNOgEF3KgCUMCNiI8CbgR4FHAjnqOA548BXDTDjPdMcFGmD7eM4yyKM2pk9A4ARyIGJnqzIEBeFdIE85wInCLQyQpaH84j4rjhKA66os94z9CIl8YyuM4TNrCiXXQafaMhG5QwiO8zQCRiQrM8iYDOndAlTdhYE4V6OBCVdxmNKF8vED66Idc4MBr5aBOWkJgIUXk262yvuH4ChHevCRuDQVQDBBcsPgQpwF4cxXiVhyFFgX0CuZoAA3QhBA6gByFwAB0IgeNAmYOZScKhrCXhUEaTcA7UUWG2k3Ao20k4lO0kHMB2V0EW0v0myqTHyN95GPMJCv2SXAbXEWFtA0AlJAddjQuSkOuEbFYGH95uPKV+Rq9i/964glR1FRSs+S885Zw9eBDlAKPuwMF0VgGilFYBorRWAQLU9pG1pXkD7h2o53OZL7JWAffoPVySMC8avQDhsYkMpBTeBkmKE0Q7LsKVP/EmLycVEgm35QQUbQtm4YMUtoASE1HOkE2sgQLzu/sNTVgf7kYf6m0chvEt9YGQl1kSFz5X179pdtf/m/WGzTMHaQOjRyOgXPRgfCQb/We6CNkqBxB7b07YkonQADYu3l19/GBcxRveLeXGASG+irMsXuNA5VjiX36ni7+CiviSdZuje9QDv0QNLQm08wBR8xRQsY+CYg3RIAowdasA/Du9X8Qk8UFwF2zkR+g7oyjIS7LehDCZsUB5y8IRoq0kAP9BkoCPKcH0dYVBq408pvniK/UAoe9TbGBGlT7nmRjDFM1hQK9pBw/QgtjBA7QeBKesyuCOjHjeHTzA8+7gwZ73PCRsqaGcoUUCwp64BIQ/sg0DjMM4WeYh0IglIs6KJSLOjHGYr6MU+tACEPnMAhD+yEjPEYAOCvBvCVsSCmNEoMHoEGgwLgQajAiBhmUBsCqohgZYGlRDm6HQUI2DGhrM37ANA9TUUQ0N5m8CDeZvAg3mbwIN5m/Wa4Mul6yhDKx3apgw36thAmufKKPrTZyQ5B6F+Sak1wQxylrAXSTxkr+6EkfFunJIi5eNdkNb5AUejGo21IIrHAeDlgwxrErY+GWMGprb1kJta+keSideNoGMNXp0FYdsOkb1WAd72JfFSyP7TzDpPnb6IbheZcblqpo8qOPwN1AeSlp28nfSdciyzfKueXj6yg/ydVnW5lpe1+qRurFg17U7pN42M3aSOl2TNnN1OyTdNqZ3kk67Jm3mOuuatLH82D0ojtckuWn1iOlBT6o6hQo/nE46pW7N2OyUtM0bp1Zn4bDBaY9PQEyGKkgN0FFKaoBemlLD9BKXGqa7ytQYB+X2hX4LeMXfK5SKHKv1Go0Kwe4eT3/N2WTsPoDZ4z2096xxFaXUaAWyesyK7cQdtTG7ByA1RvdIpMboHpLUGN1ikzJ9vyClhukerdQY3cOWGqN//DJ145epG79MTPwyMfFLp5WgxujeXFBj9JetCZCtTktCjdFPtiZGtiZAtiZAtiZAtpaubC1d2VoY2VoY2VoA2VoA2VoA2VoA2VoA2Q7tCSjTD5OtBZCtBZCtBZCtrStbW1e2Nka2Nka2NkC2NkC2NkC2NkC2NkC2tqZsbYxsbYBsbYBsbYBsHV3ZOrqydTCydTCydQCydQCydQCydQCydQCydTRl62Bk6wBk6wBk6wBk6+rK1tWVrYuRrYuRrQuQrQuQrQuQrQuQrQuQraspWxcjWxcgWxcg2ybGQU+VM6KqVwImA0ZRla8X9Jgik8X6Un9NfWdQdtK/XGqwHu9OvIrjG6P1FUrL6oESLMIgFgPf9w0cxPKLz+f1l5OGfbWk68PIlzfEHG1jQNTunLQxKGObXZM2Ooa21TVpo3Fq212TNipI+2AgFiIt18WwaqqR+rRj6okivdsxfdPQ044pm3aedUzZNPO8Y0rH4BF7P7nT1Vhutfq1ATHpCDFVQ5j9KFNOG3TnTg3RmUQ1RGc21RD9aFXiDOBXjdWfaDXWQMZNfcY1ZKuG6M24CWLcBDJuAhk3UYxb+oxb+oxrRGw1xDDGLSDjFpBxC8W4rc+4rc+4rc+4bmWtxNFg3AYybqMYd/QZd/QZd/QZd0CMO0DGHSDjDopxV59xV59xV59xF8S4C2TcBTLu9mNcjMJodK9q6Xu202ope1bWtZQ9I3Yt5ZDuVS350O5VDWJo96pJ2cDuVZ27gd2rOokDu1d1Ngd2rxq0DuxetfI7sHvVSvTA7pWacVOfcQ3ZDuxetTFughg3gYybQMZNFOOWPuOWPuMaEXtg90rJuAVk3AIybqEYt/UZt/UZt/UZ162sB3avDjJuAxm3UYw7+ow7+ow7+ow7IMYdIOMOkHEHxbirz7irz7irz7gLYtwFMu4CGVd1r8a7e15V2/2xu7P7DUPd1F/4EZfe+/XdqPziQ658ypEn5kUp9wErbxJFllOTMk8B1MzMW7HcvPJbUmVm8lux1atH5ZdiD2St+rysKMrWDOXtpV23s6zyzp1J1sNlFx9C3ym34KKHpcovVSkKyTca61hKVqZFWGyZxn68j3yGcSt3CytK698RicZuOKdh+JEUt8ebA/eGdJkVlyens7YbFsUn8tQIiYgeaojxboGKQ7l1m8Lwxaf2y896bj20fNPxoN3l+5D6Ju/r1HK2f3LOrvILXp4yywkVtpVU3s7ib5GgsDZhmX+O9px+TycFcUF0swc1UT91ydWhjQfJV9XGgztX9jce5BfbNx7kV2obD3o8gJUlOn1rT/lCMmZSfrcIbmcjIkLb9jRf4MPXarxt7F1YTdXX9y6UJ2s7ECoIbA+BlRn3qapttNfG0k5UjPhHVdsutBFWY15NWi3OVtsm3lC6+cRzGpdHH4KIptIk1Z6KC/6lQfa8VrGpotxicVbaLi6+4fbhW1jRUhpQ5vN/h+mgeLOn4k2Y4s0/keL5ErGG4uVJTcWbSsWbYMVLV3mAtLbqHxAFJl2jwOQPGwX0nOhgFLB6RgELFgWs3lHgeZBhztr2H54hFG0pFW2BFW19F4o2H1D0H8EhDqrT7qlOG6ZO+3jqDOQfZjwMOZoqtJUqtMEqtJ9ShbO6CG21CK3HEuET8H5QbE5PsTkwsTnfUVWoKS5HKS4HLC7nexCX/WxrOE0xuT3F5MLE5D6TmsuZ83/7RuebXW5NfhVEbDzwpYtQlqtUlgtWlvuUyrLrylILy3mSWusROD+osmlPlU1hKps+UZV1bFVNlaqaglU1/Q5U5R6lujq2imY9VTSDqWj2TOoqc8r/dbH4a8hAx0ypqhlYVbOnUNWDOpo+Se30CCwf1NW8p67mMF3Nn6h2OraO5kodzcE6mj9LHc2OUh8dTTfi8wAdRSPu1ReM/CKBgle+u/P4yedStg4hCnVSleqGJlHLKGzVjnBbBmblyaHCK+zVSgZKcTUveIiWNm015FOMRXDlMCvZ1cGXnDsWybO48vmI+3ROQvml+sPa+hO4QLtKy52UOwq1vF1fq9stnFV+US71eCbt8gZvE+co02mVoVRcoKS66woPsdKmVrZorPgRhC0z2fLqs+twPT6x7dKTX+SpPhS0z2/zS0I95dZkz2xlT/LwbKYwxAfoOwYkca9+NJLfvFeZba+hfdBS9in/18X/NCchijK3GgQVEmpMPGSaw7X3zmy5uOWrV0JwF+Ie0FZBH9vSB5Xaxy93NlPQ9896CZRc8D0anljQ7Z66U/qDlkI5bpOxh2zW5r+bV35t/ba4P2X+XKxIF9YcYEdeh4i5skIf3HtO9xd6P3ZW4+2zPbRSVRwVfiRWvPPV6qwZzj/ZKBeeyyOUqo9Z00g32X4TT+Wcta/maVfC1QK4tkp4IfFL06TMv8NzskFZqtHYqeaW9gxY/kp/+h9QSwMEFAAAAAgAAAAhAFtt/ZMDAQAA8QEAABQAAAB3b3JkL3dlYlNldHRpbmdzLnhtbJXRwUoDMRAG4LvgOyy5t9kWFVm6LYhUvIigPkCazrbBTCbMpK716R1rrUgv9ZZJMh8z/JPZO8bqDVgCpdaMhrWpIHlahrRqzcvzfHBtKikuLV2kBK3ZgpjZ9Pxs0jc9LJ6gFP0plSpJGvStWZeSG2vFrwGdDClD0seOGF3RklcWHb9u8sATZlfCIsRQtnZc11dmz/ApCnVd8HBLfoOQyq7fMkQVKck6ZPnR+lO0nniZmTyI6D4Yvz10IR2Y0cURhMEzCXVlqMvsJ9pR2j6qdyeMv8Dl/4DxAUDf3K8SsVtEjUAnqRQzU82AcgkYPmBOfMPUC7D9unYxUv/4cKeF/RPU9BNQSwMEFAAAAAgAM2tQVmndDRX5BQAASxsAABUAAAB3b3JkL3RoZW1lL3RoZW1lMS54bWztWV2v0zYY/itW7ks+mqQJoqB+wgYHEOeMiUs3cRNznDiK3XNOhZAmuJw0aRqbdjGk3e1i2oYE0m7Yrzkb08Yk/sIcN22T1oGxlQkkWukcfzzP68fva7920nMXThICjlDOME27mnnG0ABKAxriNOpqMz5teRpgHKYhJDRFXW2OmHbh/Dl4lscoQUCwUybKiel0tZjz7Kyus0B0QXYmwUFOGZ3yMwFNdDqd4gDpkpYQ3TJMS08gTrXSBtzi0wylom9K8wRyUc0jPczhsVAm+YZb8lOYCGHXpH1wUNjXVgJHRPxJOSsaApLvF6ZRjSGx4aFZ/GNzNiA5OIKkq4lxQnp8gE64BghkXHR0NUN+NKCfP6evWIQ3kCvEsfwsiSUjPLQkMY8mK6YxsjzbXI8gEYRvA0de8V1blAgYBGK25hbYdFzDs5bgCmpRVFj3O2Z7g1AZob09gu/2LbtOkKhF0d6e6NgfDZ06QaIWRWeL0DOsvt+uEyRqUXS3CPao17FGdYJExQSnh9twt+N57hK+wkwpuaTE+65rdIZL/BqmV1bawkDKm9ZdAm/TfCwAMsqQ4xTweYamMBC4XsYpA0PMMgLnGshgSploNizTFIvQNqzVd+F3eBbBCr1sC9h2WyEJsCDHGe9qHwrDWgXz4ukPL54+Bqf3npze+/n0/v3Tez+paJdgGlVpz7/7/K+Hn4A/H3/7/MGXDQRWJfz246e//vJFA5JXkc++evT7k0fPvv7sj+8fqPC9HE6q+AOcIAauomNwgybF5BRDoEn+mpSDGOIqpZdGDKawIKngIx7X4FfnkEAVsI/qjryZi+ShRF6c3a6J3o/zGccq5OU4qSH3KCV9mqsndlkOV/HFLI0axs9nVeANCI+Uww82Qj2aZWL9Y6XRQYxqUq8TEX0YoRRxUPTRQ4RUvFsY1/y7tzxtwC0M+hCrHXOAJ1zNuoQTEaA5bAh9zUN7N0GfEuUAQ3RUh4ptAonSKCI1b16EMw4TtWqYkCr0CuSxUuj+PA9qjmdcBD1ChIJRiBhTkq7lxazXpMtQJDL1Ctgj86QOzTk+VEKvQEqr0CE9HMQwydS6cRpXwR+wQ7FiIbhOuVoHre+Zoi4CAtPmyN/EiL/mjv8IR7F6sRQ9s1y5RxCt79E5mUK0MK9vJPwEp6/I/v971hdJ9tk3D9+xfN/LsXqLbWb5RuBmbh/QPMTvRmofwll6HRXb531mf5/Z32f2l+zyN5HP1ylcr171pZ2k8d4/xYTs8zlBV5hM/kwcXuFYNMqKJK2eM7JYFJfj1YBRDmUZ5JR/jHm8H8NMjGPKISJW2o4YyCgTJ4jWaFyeP7Nkj4blw5y5eswVDMjXHYaz7hDnFV80u53KU/FqBFmLWFVDwX4dHdXh6jraKh2d9j/UIee3GyG+SohnvlSIXgmPuGsBcbQK39jlywUWQILCImClgWWcdx7zRpfW526ppujbu4t5TUd17dV1VBdlDEO01b7jqPuV2NYkWmolHe/NRF3fThgkrdfAsdiFbUeQA5h1tam4TYpikgmDrLiDQBKJ13sBL/39r9JNljM+hCxe4GRX6YMEc5QDghOx8mvRIOlanmmJLPE26/PFJn8L9emb0UbTKQp4Q8u6KvpKK8ru/4ouKnQmdO/H4TGYkFl+AwpvOR2z8GKIGV+5NMR5ZaGvXbmRw8qdWXtJuN6xkGQxLI+bWppf4GV5pacyESl1c1r1ejmbSTTeybH8atZGJm06W4pTtSGfvLl7QEVXu0GXo05/vvfKA+S/HxUVeV6DvHaDvMZzZZe3hsqA62XaeHzs/JzYXMN65Roqa1u/itDJbbEPhuJ6OyOclW8UTsRrIyFpwStTg2xeJpwTDmY57mp3DKdnDyxn0DI8Z9Sy27bR8pxeu9VznLY5ckxj2LfuCs/In4gWo4/FWy4y38lPR4qffgAWzrnjWmO/7ffdlt/ujVv2sO+1/IHbbw3dQWc4Hg4czx/f1cCRBNu99sB2R17LNQeDlu0ahXzPb3Vsy+rZnZ43snsCXKbHkzKflM5Y+vT831BLAwQUAAAACAAFbmZZ8IgaroYCAAA2CAAAEQAcAHdvcmQvZG9jdW1lbnQueG1sIKIYACigFAAAAAAAAAAAAAAAAAAAAAAAAAAAAKWVS27bMBBAr6Jq3YT6OLYrxAla59MsCgTNomuaoiQiEocgacvu1brokXqFDinLVhIgsOOFRA7JefOThv/+/L28Xjd1sOLaCJCzMD6PwoBLBrmQ5Sxc2uJsGgbGUpnTGiSfhRtuwuuryzbLgS0bLm2AAGmyVrFZWFmrMkIMq3hDzXkjmAYDhT1n0BAoCsE4aUHnJIniyM+UBsaNQWtzKlfUhFtc85YGikvcLEA31KKoS9JQ/bxUZ0hX1IqFqIXdIDsa9xjAGLTMtoiznUNOJesc2g69hj7Ebqdys82At0g0r9EHkKYSah/GR2m4WfWQ1XtBrJq6P9eqeHRaDW40bXHYAw9xP++Umrrz/H1iHB1QEYfYaRziwkubvScNFXJv+EOpGSQ3vjgOkLwGqPK04txrWKo9TZxGe5DPO5b7r49gbYs8DM2c5sxTRdXuD2zjsTnOofhi6xAxm4avByAm8uNIfWgENQec4yDTN9/OmPH1xxgENYeZyW1eHUVK+i+ZOF1qaUUNtpaGZQ+lBE0XNSYbf48Av/DAtZDAF8C9MQdu8FO+DnrzoWv/C8g3blReJ1NU0wfMdTKPJtPbcXpqT3LJ82DL19aDvyTj9DaaeOM6wMeI/OcsHN2l36bzNO3WH3VA3MRefed1DZ+DX6Dr/NMlcUvurV+pT6ZJOr/7Onqt/lIF38qtG87so0eo8uk3UrA7xUkywkuzzbAs8cW0m4MW2MpnoQJtNRU27Liq/EGdcQvYWeNRd1aLssKjvbgAawHvjV6ueTHYrTjNOd5Rk8SLBYAdiOXSehEFb49BbXDZKMqwyP6QX8e7+1674ma1kPxRWIbOp+NuG4Pt48RpV2ec9Pf91X9QSwMEFAAAAAgABW5mWXz5ghLhAAAAQQIAAAsAAABfcmVscy8ucmVsc52SS04DMQyGrxJ53/G0SAihpt100x1CvYCVeGYimocS98HZWHAkrkDoBiLxUpe2f3/6HOXt5XW5Pvu9OnIuLgYN864HxcFE68Ko4SDD7A7Wq+Uj70lqokwuFVVXQtEwiaR7xGIm9lS6mDjUyRCzJ6llHjGReaKRcdH3t5i/MqBlqt1z4v8Q4zA4w5toDp6DfANGPgsHy3aWct3P4riA2lEeWTTYaB5quyCl1FU0qK3VkLf2BhReqfTzkehZyJIQmpj5d6GPRGO0uN7o70dqE586p5gtVqdLu9GZX3Sw+Qird1BLAwQUAAAACAAFbmZZvn2nPeMAAAAmAwAAHAAAAHdvcmQvX3JlbHMvZG9jdW1lbnQueG1sLnJlbHO1kj1uwzAMha8icK9lpz8oiihZumRNfQFFpmwjtiSITNqcrUOP1CtUcIHWQjN08fgeyfe+gZ/vH+vt2ziIM0bqvVNQFSUIdMY3vWsVnNjePMJ2s97joDltUNcHEunEkYKOOTxJSabDUVPhA7o0sT6OmpOMrQzaHHWLclWWDzLOMyDPFPUl4H8SvbW9wWdvTiM6vhIsX/HwgsyJn0DUOrbICmZmkRJB7BoFcdfcgpCLkdAfDLrGsFqUgS8DzgkmnfVXS/ZzusXf+kl+m1UGcb8khPWOa30YZiA/VkZxN1HI7Ns3X1BLAwQUAAAACAAccmZZUlo+hkwBAAAaBQAAEwAAAFtDb250ZW50X1R5cGVzXS54bWy1lE1OwzAQha9ieVslblkghJp2AWyhEr2A60xSC8e27Onf2VhwJK7AJGkjhEqDaLuJlMy8972xMv58/xhPt5VhawhRO5vxUTrkDKxyubZlxldYJHd8OhnPdx4io1YbM75E9PdCRLWESsbUebBUKVyoJNJrKIWX6k2WIG6Gw1uhnEWwmGDtwSfjRyjkyiB72tLnFhvARM4e2saalXHpvdFKItXF2uY/KMmekJKy6YlL7eOAGjgTRxFN6VfCQfhCJxF0DmwmAz7LitrExoVc5E6tKpKmp32OJHVFoRV0+trNB6cgRjriyqRdpZLaDnqDRNwZiJeP0fr+gQ+IpLhGgr1zf4YNLF6vFuObeX+SgsBzuTBw+RyddX8KpEWE9jk6O0hjc5JJrbPgfKTNDv8Y/LC6tTqhkT0E1D2/Xock77MnhPpWyCE/BhfNTTf5AlBLAQIUABQAAAAIAK+YSUqNEzDqOgEAAKcCAAAQAAAAAAAAAAAAAAAAAAAAAABkb2NQcm9wcy9hcHAueG1sUEsBAhQAFAAAAAgABW5mWVDfgmNYAQAAjAIAABEAAAAAAAAAAAAAAAAAaAEAAGRvY1Byb3BzL2NvcmUueG1sUEsBAhQAFAAAAAgABW5mWZ5WdX44AgAAAgkAABIAAAAAAAAAAAAAAAAA7wIAAHdvcmQvZm9udFRhYmxlLnhtbFBLAQIUABQAAAAIAAVuZlnBTYlGHgQAAA4MAAARAAAAAAAAAAAAAAAAAFcFAAB3b3JkL3NldHRpbmdzLnhtbFBLAQIUABQAAAAIAAVuZll/jXz0SQ4AAOeiAAAPAAAAAAAAAAAAAAAAAKQJAAB3b3JkL3N0eWxlcy54bWxQSwECFAAUAAAACAAAACEAW239kwMBAADxAQAAFAAAAAAAAAAAAAAAAAAaGAAAd29yZC93ZWJTZXR0aW5ncy54bWxQSwECFAAUAAAACAAza1BWad0NFfkFAABLGwAAFQAAAAAAAAAAAAAAAABPGQAAd29yZC90aGVtZS90aGVtZTEueG1sUEsBAhQAFAAAAAgABW5mWfCIGq6GAgAANggAABEAAAAAAAAAAAAAAAAAex8AAHdvcmQvZG9jdW1lbnQueG1sUEsBAhQAFAAAAAgABW5mWXz5ghLhAAAAQQIAAAsAAAAAAAAAAAAAAAAATCIAAF9yZWxzLy5yZWxzUEsBAhQAFAAAAAgABW5mWb59pz3jAAAAJgMAABwAAAAAAAAAAAAAAAAAViMAAHdvcmQvX3JlbHMvZG9jdW1lbnQueG1sLnJlbHNQSwECFAAUAAAACAAccmZZUlo+hkwBAAAaBQAAEwAAAAAAAAAAAAAAAABzJAAAW0NvbnRlbnRfVHlwZXNdLnhtbFBLBQYAAAAACwALAMECAADwJQAAAAA=" - ) - - expected_result_pdf = "Hello, World!" - expected_result_docx = "Hello, World!!" - - async def setup_after_prep(self, module_test): - module_test.set_expect_requests( - {"uri": "/"}, - {"response_data": ''}, - ) - module_test.set_expect_requests( - {"uri": "/Test_PDF"}, - {"response_data": self.pdf_data, "headers": {"Content-Type": "application/pdf"}}, - ) - module_test.set_expect_requests( - {"uri": "/Test_DOCX"}, - { - "response_data": self.docx_data, - "headers": {"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, - }, - ) - - def check(self, module_test, events): - filesystem_events = [e for e in events if e.type == "FILESYSTEM"] - assert 2 == len(filesystem_events), filesystem_events - for filesystem_event in filesystem_events: - file = Path(filesystem_event.data["path"]) - assert file.is_file(), "Destination file doesn't exist" - assert open(file, "rb").read() == self.pdf_data or open(file, "rb").read() == self.docx_data, ( - f"File at {file} does not contain the correct content" - ) - raw_text_events = [e for e in events if e.type == "RAW_TEXT"] - assert 2 == len(raw_text_events), "Failed to emit RAW_TEXT event" - for raw_text_event in raw_text_events: - assert raw_text_event.data in [ - self.expected_result_pdf, - self.expected_result_docx, - ], f"Text extracted from {raw_text_event.data['path']} is incorrect, got {raw_text_event.data}" diff --git a/bbot/test/test_step_2/module_tests/test_module_ffuf.py b/bbot/test/test_step_2/module_tests/test_module_ffuf.py index 3df659e159..854fb12a28 100644 --- a/bbot/test/test_step_2/module_tests/test_module_ffuf.py +++ b/bbot/test/test_step_2/module_tests/test_module_ffuf.py @@ -23,8 +23,8 @@ async def setup_before_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - assert any(e.type == "URL_UNVERIFIED" and "admin" in e.data for e in events) - assert not any(e.type == "URL_UNVERIFIED" and "11111111" in e.data for e in events) + assert any(e.type == "URL_UNVERIFIED" and "admin" in e.url for e in events) + assert not any(e.type == "URL_UNVERIFIED" and "11111111" in e.url for e in events) class TestFFUF2(TestFFUF): @@ -41,8 +41,8 @@ async def setup_before_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - assert any(e.type == "URL_UNVERIFIED" and "console" in e.data for e in events) - assert not any(e.type == "URL_UNVERIFIED" and "11111111" in e.data for e in events) + assert any(e.type == "URL_UNVERIFIED" and "console" in e.url for e in events) + assert not any(e.type == "URL_UNVERIFIED" and "11111111" in e.url for e in events) class TestFFUF_ignorecase(TestFFUF): @@ -65,8 +65,8 @@ async def setup_before_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - assert any(e.type == "URL_UNVERIFIED" and "admin" in e.data for e in events) - assert not any(e.type == "URL_UNVERIFIED" and "Admin" in e.data for e in events) + assert any(e.type == "URL_UNVERIFIED" and "admin" in e.url for e in events) + assert not any(e.type == "URL_UNVERIFIED" and "Admin" in e.url for e in events) class TestFFUFHeaders(TestFFUF): @@ -86,5 +86,5 @@ async def setup_before_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - assert any(e.type == "URL_UNVERIFIED" and "console" in e.data for e in events) - assert not any(e.type == "URL_UNVERIFIED" and "11111111" in e.data for e in events) + assert any(e.type == "URL_UNVERIFIED" and "console" in e.url for e in events) + assert not any(e.type == "URL_UNVERIFIED" and "11111111" in e.url for e in events) diff --git a/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py b/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py index 4a5748f8fb..fe2d923aaa 100644 --- a/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +++ b/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py @@ -200,21 +200,21 @@ def check(self, module_test, events): for e in events: if e.type == "URL_UNVERIFIED": - if e.data == "http://127.0.0.1:8888/administrator.aspx": + if e.url == "http://127.0.0.1:8888/administrator.aspx": basic_detection = True - if e.data == "http://127.0.0.1:8888/directory/": + if e.url == "http://127.0.0.1:8888/directory/": directory_detection = True - if e.data == "http://127.0.0.1:8888/adm_portal.aspx": + if e.url == "http://127.0.0.1:8888/adm_portal.aspx": prefix_detection = True - if e.data == "http://127.0.0.1:8888/abcconsole.aspx": + if e.url == "http://127.0.0.1:8888/abcconsole.aspx": delimiter_detection = True - if e.data == "http://127.0.0.1:8888/adm_directory/": + if e.url == "http://127.0.0.1:8888/adm_directory/": directory_delimiter_detection = True - if e.data == "http://127.0.0.1:8888/xyzdirectory/": + if e.url == "http://127.0.0.1:8888/xyzdirectory/": prefix_delimiter_detection = True - if e.data == "http://127.0.0.1:8888/short.pl": + if e.url == "http://127.0.0.1:8888/short.pl": short_extensions_detection = True - if e.data == "http://127.0.0.1:8888/newproxy.aspx": + if e.url == "http://127.0.0.1:8888/newproxy.aspx": subword_detection = True assert basic_detection diff --git a/bbot/test/test_step_2/module_tests/test_module_generic_ssrf.py b/bbot/test/test_step_2/module_tests/test_module_generic_ssrf.py deleted file mode 100644 index c0911fd661..0000000000 --- a/bbot/test/test_step_2/module_tests/test_module_generic_ssrf.py +++ /dev/null @@ -1,88 +0,0 @@ -import re -import asyncio -from werkzeug.wrappers import Response - -from .base import ModuleTestBase - - -def extract_subdomain_tag(data): - pattern = r"http://([a-z0-9]{4})\.fakedomain\.fakeinteractsh\.com" - match = re.search(pattern, data) - if match: - return match.group(1) - - -class TestGeneric_SSRF(ModuleTestBase): - targets = ["http://127.0.0.1:8888"] - modules_overrides = ["httpx", "generic_ssrf"] - - def request_handler(self, request): - subdomain_tag = None - - if request.method == "GET": - subdomain_tag = extract_subdomain_tag(request.full_path) - elif request.method == "POST": - subdomain_tag = extract_subdomain_tag(request.data.decode()) - if subdomain_tag: - asyncio.run( - self.interactsh_mock_instance.mock_interaction( - subdomain_tag, msg=f"{request.method}: {request.data.decode()}" - ) - ) - - return Response("alive", status=200) - - async def setup_before_prep(self, module_test): - self.interactsh_mock_instance = module_test.mock_interactsh("generic_ssrf") - module_test.monkeypatch.setattr( - module_test.scan.helpers, "interactsh", lambda *args, **kwargs: self.interactsh_mock_instance - ) - - async def setup_after_prep(self, module_test): - expect_args = re.compile("/") - module_test.set_expect_requests_handler(expect_args=expect_args, request_handler=self.request_handler) - - def check(self, module_test, events): - total_vulnerabilities = 0 - total_findings = 0 - - for e in events: - if e.type == "VULNERABILITY": - total_vulnerabilities += 1 - elif e.type == "FINDING": - total_findings += 1 - - assert total_vulnerabilities == 30, "Incorrect number of vulnerabilities detected" - assert total_findings == 30, "Incorrect number of findings detected" - - assert any( - e.type == "VULNERABILITY" - and "Out-of-band interaction: [Generic SSRF (GET)]" - and "[Triggering Parameter: Dest]" in e.data["description"] - for e in events - ), "Failed to detect Generic SSRF (GET)" - assert any( - e.type == "VULNERABILITY" and "Out-of-band interaction: [Generic SSRF (POST)]" in e.data["description"] - for e in events - ), "Failed to detect Generic SSRF (POST)" - assert any( - e.type == "VULNERABILITY" and "Out-of-band interaction: [Generic XXE] [HTTP]" in e.data["description"] - for e in events - ), "Failed to detect Generic SSRF (XXE)" - - -class TestGeneric_SSRF_httponly(TestGeneric_SSRF): - config_overrides = {"modules": {"generic_ssrf": {"skip_dns_interaction": True}}} - - def check(self, module_test, events): - total_vulnerabilities = 0 - total_findings = 0 - - for e in events: - if e.type == "VULNERABILITY": - total_vulnerabilities += 1 - elif e.type == "FINDING": - total_findings += 1 - - assert total_vulnerabilities == 30, "Incorrect number of vulnerabilities detected" - assert total_findings == 0, "Incorrect number of findings detected" diff --git a/bbot/test/test_step_2/module_tests/test_module_git_clone.py b/bbot/test/test_step_2/module_tests/test_module_git_clone.py index 17333cbcbb..34e21875e0 100644 --- a/bbot/test/test_step_2/module_tests/test_module_git_clone.py +++ b/bbot/test/test_step_2/module_tests/test_module_git_clone.py @@ -190,6 +190,7 @@ def new_filter_event(event): event.data["url"] = event.data["url"].replace( "https://github.com/blacklanternsecurity", f"file://{temp_path}" ) + event.parsed_url = module_test.scan.helpers.urlparse(event.data["url"]) return old_filter_event(event) module_test.monkeypatch.setattr(module_test.scan.modules["git_clone"], "filter_event", new_filter_event) diff --git a/bbot/test/test_step_2/module_tests/test_module_github_codesearch.py b/bbot/test/test_step_2/module_tests/test_module_github_codesearch.py index 2bd9993b20..7c161885aa 100644 --- a/bbot/test/test_step_2/module_tests/test_module_github_codesearch.py +++ b/bbot/test/test_step_2/module_tests/test_module_github_codesearch.py @@ -76,7 +76,7 @@ def check(self, module_test, events): [ e for e in events - if e.type == "URL_UNVERIFIED" and e.data == self.github_file_url and e.scope_distance == 2 + if e.type == "URL_UNVERIFIED" and e.url == self.github_file_url and e.scope_distance == 2 ] ), "Failed to emit URL_UNVERIFIED" assert 1 == len( @@ -90,7 +90,7 @@ def check(self, module_test, events): ] ), "Failed to emit CODE_REPOSITORY" assert 1 == len( - [e for e in events if e.type == "URL" and e.data == self.github_file_url and e.scope_distance == 2] + [e for e in events if e.type == "URL" and e.url == self.github_file_url and e.scope_distance == 2] ), "Failed to visit URL" assert 1 == len( [ diff --git a/bbot/test/test_step_2/module_tests/test_module_github_org.py b/bbot/test/test_step_2/module_tests/test_module_github_org.py index d8003fd2a5..b7290a2660 100644 --- a/bbot/test/test_step_2/module_tests/test_module_github_org.py +++ b/bbot/test/test_step_2/module_tests/test_module_github_org.py @@ -5,11 +5,12 @@ class TestGithub_Org(ModuleTestBase): config_overrides = {"modules": {"github_org": {"api_key": "asdf"}}} modules_overrides = ["github_org", "speculate"] - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): await module_test.mock_dns( {"blacklanternsecurity.com": {"A": ["127.0.0.99"]}, "github.com": {"A": ["127.0.0.99"]}} ) + async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( url="https://api.github.com/zen", match_headers={"Authorization": "token asdf"} ) @@ -416,7 +417,7 @@ def check(self, module_test, events): e for e in events if e.type == "URL_UNVERIFIED" - and e.data == "https://github.com/blacklanternsecurity" + and e.url == "https://github.com/blacklanternsecurity" and e.scope_distance == 1 ] ) diff --git a/bbot/test/test_step_2/module_tests/test_module_gitlab_onprem.py b/bbot/test/test_step_2/module_tests/test_module_gitlab_onprem.py index 7d14598f6e..872a549f20 100644 --- a/bbot/test/test_step_2/module_tests/test_module_gitlab_onprem.py +++ b/bbot/test/test_step_2/module_tests/test_module_gitlab_onprem.py @@ -167,7 +167,7 @@ def check(self, module_test, events): e for e in events if e.type == "TECHNOLOGY" - and e.data["technology"] == "GitLab" + and e.data["technology"] == "gitlab" and e.data["url"] == "http://127.0.0.1:8888/" ] ) diff --git a/bbot/test/test_step_2/module_tests/test_module_gowitness.py b/bbot/test/test_step_2/module_tests/test_module_gowitness.py index 528cc3abec..ef87ade3be 100644 --- a/bbot/test/test_step_2/module_tests/test_module_gowitness.py +++ b/bbot/test/test_step_2/module_tests/test_module_gowitness.py @@ -39,6 +39,7 @@ async def setup_after_prep(self, module_test): async def new_emit_event(event, **kwargs): if event.data["url"] == "https://github.com/blacklanternsecurity": event.data["url"] = event.data["url"].replace("https://github.com", "http://127.0.0.1:8888") + event.parsed_url = module_test.scan.helpers.urlparse(event.data["url"]) await old_emit_event(event, **kwargs) module_test.monkeypatch.setattr(module_test.scan.modules["social"], "emit_event", new_emit_event) @@ -55,11 +56,9 @@ def check(self, module_test, events): assert len(screenshots) == 1, ( f"{len(screenshots):,} .jpeg files found at {screenshots_path}, should have been 1" ) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/"]) - assert 1 == len( - [e for e in events if e.type == "URL_UNVERIFIED" and e.data == "https://fonts.googleapis.com/"] - ) - assert 0 == len([e for e in events if e.type == "URL" and e.data == "https://fonts.googleapis.com/"]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/"]) + assert 1 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.url == "https://fonts.googleapis.com/"]) + assert 0 == len([e for e in events if e.type == "URL" and e.url == "https://fonts.googleapis.com/"]) assert 1 == len( [e for e in events if e.type == "SOCIAL" and e.data["url"] == "http://127.0.0.1:8888/blacklanternsecurity"] ) @@ -134,3 +133,55 @@ def check(self, module_test, events): filename = Path(webscreenshot.data["path"]) # sadly this file doesn't exist because gowitness doesn't truncate properly assert not filename.exists() + + +class TestGowitness_MultiPort(ModuleTestBase): + """ + Integration test: two URLs on the same host with different ports + (HTTP :8888 and HTTPS :9999) both get correctly correlated screenshots. + Exercises the real gowitness binary and _resolve_parent tiered lookup. + """ + + targets = ["http://127.0.0.1:8888", "https://127.0.0.1:9999"] + modules_overrides = ["gowitness", "httpx"] + + import shutil + + home_dir = Path("/tmp/.bbot_gowitness_multiport_test") + shutil.rmtree(home_dir, ignore_errors=True) + config_overrides = { + "force_deps": True, + "home": str(home_dir), + "omit_event_types": [], + } + + async def setup_after_prep(self, module_test): + # HTTP server on port 8888 + module_test.set_expect_requests( + respond_args={ + "response_data": "Port 8888Port 8888", + "headers": {"Server": "Apache/2.4.41"}, + }, + ) + # HTTPS server on port 9999 + module_test.httpserver_ssl.expect_request("/").respond_with_data( + "Port 9999Port 9999", + headers={"Server": "nginx/1.18.0"}, + ) + + def check(self, module_test, events): + webscreenshots = [e for e in events if e.type == "WEBSCREENSHOT"] + assert len(webscreenshots) >= 2, f"Expected at least 2 WEBSCREENSHOT events, got {len(webscreenshots)}" + + screenshot_urls = {e.data["url"] for e in webscreenshots} + assert any("8888" in url for url in screenshot_urls), f"No screenshot for port 8888. URLs: {screenshot_urls}" + assert any("9999" in url for url in screenshot_urls), f"No screenshot for port 9999. URLs: {screenshot_urls}" + + # Verify parent events reference the correct port + for ws in webscreenshots: + url = ws.data["url"] + parent = ws.parent + if "8888" in url: + assert "8888" in str(parent.data), f"Screenshot for :8888 has wrong parent: {parent.data}" + elif "9999" in url: + assert "9999" in str(parent.data), f"Screenshot for :9999 has wrong parent: {parent.data}" diff --git a/bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py b/bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py index f6a47671c7..dd0380f653 100644 --- a/bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py +++ b/bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py @@ -31,4 +31,4 @@ def check(self, module_test, events): finding = [e for e in events if e.type == "FINDING"] assert finding, "should have raised 1 FINDING event" assert finding[0].data["url"] == "http://127.0.0.1:8888/" - assert finding[0].data["description"] == "GraphQL schema" + assert finding[0].data["description"] == "GraphQL Schema at http://127.0.0.1:8888/" diff --git a/bbot/test/test_step_2/module_tests/test_module_host_header.py b/bbot/test/test_step_2/module_tests/test_module_host_header.py index a2d69e9b57..8eb137022e 100644 --- a/bbot/test/test_step_2/module_tests/test_module_host_header.py +++ b/bbot/test/test_step_2/module_tests/test_module_host_header.py @@ -35,9 +35,15 @@ def request_handler(self, request): async def setup_before_prep(self, module_test): self.interactsh_mock_instance = module_test.mock_interactsh("host_header") - module_test.monkeypatch.setattr( - module_test.scan.helpers, "interactsh", lambda *args, **kwargs: self.interactsh_mock_instance - ) + + # Mock at the helper creation level BEFORE modules are set up + def mock_interactsh_factory(*args, **kwargs): + return self.interactsh_mock_instance + + # Apply the mock to the core helpers so modules get the mock during setup + from bbot.core.helpers.helper import ConfigAwareHelper + + module_test.monkeypatch.setattr(ConfigAwareHelper, "interactsh", mock_interactsh_factory) async def setup_after_prep(self, module_test): expect_args = re.compile("/") diff --git a/bbot/test/test_step_2/module_tests/test_module_http.py b/bbot/test/test_step_2/module_tests/test_module_http.py index 2bc99f5ddf..df90b78525 100644 --- a/bbot/test/test_step_2/module_tests/test_module_http.py +++ b/bbot/test/test_step_2/module_tests/test_module_http.py @@ -52,12 +52,3 @@ def check(self, module_test, events): assert self.headers_correct is True assert self.method_correct is True assert self.url_correct is True - - -class TestHTTPSIEMFriendly(TestHTTP): - modules_overrides = ["http"] - config_overrides = {"modules": {"http": dict(TestHTTP.config_overrides["modules"]["http"])}} - config_overrides["modules"]["http"]["siem_friendly"] = True - - def verify_data(self, j): - return j["data"] == {"DNS_NAME": "blacklanternsecurity.com"} and j["type"] == "DNS_NAME" diff --git a/bbot/test/test_step_2/module_tests/test_module_httpx.py b/bbot/test/test_step_2/module_tests/test_module_httpx.py index fe8a2681b3..03b69ff8e6 100644 --- a/bbot/test/test_step_2/module_tests/test_module_httpx.py +++ b/bbot/test/test_step_2/module_tests/test_module_httpx.py @@ -68,9 +68,9 @@ async def setup_after_prep(self, module_test): def check(self, module_test, events): assert 1 == len( - [e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and "status-301" in e.tags] + [e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and "status-301" in e.tags] ) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "https://127.0.0.1:9999/"]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "https://127.0.0.1:9999/"]) class TestHTTPX_Redirect(ModuleTestBase): @@ -84,13 +84,13 @@ async def setup_after_prep(self, module_test): def check(self, module_test, events): assert 1 == len( - [e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and "status-301" in e.tags] + [e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/" and "status-301" in e.tags] ) assert 1 == len( [ e for e in events - if e.type == "URL_UNVERIFIED" and e.data == "http://www.evilcorp.com/" and "affiliate" in e.tags + if e.type == "URL_UNVERIFIED" and e.url == "http://www.evilcorp.com/" and "affiliate" in e.tags ] ) assert 1 == len( @@ -121,11 +121,11 @@ def check(self, module_test, events): assert 4 == len([e for e in events if e.type == "URL_UNVERIFIED"]) assert 3 == len([e for e in events if e.type == "HTTP_RESPONSE"]) assert 3 == len([e for e in events if e.type == "URL"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/test.aspx"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/test.txt"]) - assert not any(e for e in events if "URL" in e.type and ".svg" in e.data) - assert not any(e for e in events if "URL" in e.type and ".woff" in e.data) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/"]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/test.aspx"]) + assert 1 == len([e for e in events if e.type == "URL" and e.url == "http://127.0.0.1:8888/test.txt"]) + assert not any(e for e in events if "URL" in e.type and ".svg" in e.url) + assert not any(e for e in events if "URL" in e.type and ".woff" in e.url) class TestHTTPX_querystring_removed(ModuleTestBase): @@ -136,14 +136,14 @@ async def setup_after_prep(self, module_test): module_test.httpserver.expect_request("/").respond_with_data('') def check(self, module_test, events): - assert [e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/test.php"] + assert [e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/test.php"] class TestHTTPX_querystring_notremoved(TestHTTPX_querystring_removed): config_overrides = {"url_querystring_remove": False} def check(self, module_test, events): - assert [e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/test.php?foo=bar"] + assert [e for e in events if e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/test.php?foo=bar"] class TestHTTPX_custom_headers(ModuleTestBase): diff --git a/bbot/test/test_step_2/module_tests/test_module_hunt.py b/bbot/test/test_step_2/module_tests/test_module_hunt.py index 867a2565c6..a572e96a74 100644 --- a/bbot/test/test_step_2/module_tests/test_module_hunt.py +++ b/bbot/test/test_step_2/module_tests/test_module_hunt.py @@ -14,12 +14,20 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - assert any( - e.type == "FINDING" - and e.data["description"] - == "Found potentially interesting parameter. Name: [cipher] Parameter Type: [GETPARAM] Categories: [Insecure Cryptography] Original Value: [xor]" - for e in events - ) + finding_event = None + for e in events: + if ( + e.type == "FINDING" + and e.data["description"] + == "Found potentially interesting parameter. Name: [cipher] Parameter Type: [GETPARAM] Categories: [Insecure Cryptography] Original Value: [xor]" + ): + finding_event = e + break + + assert finding_event is not None + # Hunt emits INFO severity and LOW confidence + assert finding_event.data["severity"] == "INFO" + assert finding_event.data["confidence"] == "LOW" class TestHunt_Multiple(TestHunt): diff --git a/bbot/test/test_step_2/module_tests/test_module_iis_shortnames.py b/bbot/test/test_step_2/module_tests/test_module_iis_shortnames.py index 6ca827f56a..d44854bab8 100644 --- a/bbot/test/test_step_2/module_tests/test_module_iis_shortnames.py +++ b/bbot/test/test_step_2/module_tests/test_module_iis_shortnames.py @@ -90,17 +90,41 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - vulnerabilityEmitted = False + magicurl_findingEmitted = False url_hintEmitted = False zip_findingEmitted = False for e in events: - if e.type == "VULNERABILITY" and "iis-magic-url" not in e.tags: - vulnerabilityEmitted = True - if e.type == "URL_HINT" and e.data == "http://127.0.0.1:8888/BLSHAX~1": + if e.type == "FINDING" and "iis-magic-url" not in e.tags: + magicurl_findingEmitted = True + if e.type == "URL_HINT" and e.url == "http://127.0.0.1:8888/BLSHAX~1": url_hintEmitted = True if e.type == "FINDING" and "Possible backup file (zip) in web root" in e.data["description"]: zip_findingEmitted = True - assert vulnerabilityEmitted + assert magicurl_findingEmitted assert url_hintEmitted assert zip_findingEmitted + + +class TestIIS_Shortnames_GatewayError(ModuleTestBase): + """Negative test: server returns 502 gateway errors. Should NOT detect IIS shortnames.""" + + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["httpx", "iis_shortnames"] + + async def setup_after_prep(self, module_test): + module_test.httpserver.no_handler_status_code = 404 + + expect_args = {"method": "GET", "uri": "/"} + respond_args = {"response_data": "alive", "status": 200} + module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + + # Control URL returns 404, test URL returns 502 (gateway error from CDN) + expect_args = {"method": "GET", "uri": "/*~1*/a.aspx"} + respond_args = {"response_data": "Bad Gateway", "status": 502} + module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + + def check(self, module_test, events): + for e in events: + if e.type == "FINDING" and "IIS Shortname" in e.data.get("description", ""): + raise AssertionError("IIS Shortname finding should NOT be emitted when gateway errors are present") diff --git a/bbot/test/test_step_2/module_tests/test_module_json.py b/bbot/test/test_step_2/module_tests/test_module_json.py index 27ed5a55e0..61ed7fc1f3 100644 --- a/bbot/test/test_step_2/module_tests/test_module_json.py +++ b/bbot/test/test_step_2/module_tests/test_module_json.py @@ -23,13 +23,13 @@ def check(self, module_test, events): assert len(dns_json) == 1 dns_json = dns_json[0] scan = scan_json[0] - assert scan["data"]["name"] == module_test.scan.name - assert scan["data"]["id"] == module_test.scan.id + assert scan["data_json"]["name"] == module_test.scan.name + assert scan["data_json"]["id"] == module_test.scan.id assert scan["id"] == module_test.scan.id assert scan["uuid"] == str(module_test.scan.root_event.uuid) assert scan["parent_uuid"] == str(module_test.scan.root_event.uuid) - assert scan["data"]["target"]["seeds"] == ["blacklanternsecurity.com"] - assert scan["data"]["target"]["whitelist"] == ["blacklanternsecurity.com"] + assert not "seeds" in scan["data_json"]["target"], "seeds should not be in target json" + assert scan["data_json"]["target"]["target"] == ["blacklanternsecurity.com"] assert dns_json["data"] == dns_data assert dns_json["id"] == str(dns_event.id) assert dns_json["uuid"] == str(dns_event.uuid) @@ -45,26 +45,11 @@ def check(self, module_test, events): assert scan_reconstructed.data["id"] == module_test.scan.id assert scan_reconstructed.uuid == scan_event.uuid assert scan_reconstructed.parent_uuid == scan_event.uuid - assert scan_reconstructed.data["target"]["seeds"] == ["blacklanternsecurity.com"] - assert scan_reconstructed.data["target"]["whitelist"] == ["blacklanternsecurity.com"] + assert not "seeds" in scan_reconstructed.data["target"], "seeds should not be in target json" + assert scan_reconstructed.data["target"]["target"] == ["blacklanternsecurity.com"] assert dns_reconstructed.data == dns_data assert dns_reconstructed.uuid == dns_event.uuid assert dns_reconstructed.parent_uuid == module_test.scan.root_event.uuid assert dns_reconstructed.discovery_context == context_data assert dns_reconstructed.discovery_path == [context_data] assert dns_reconstructed.parent_chain == [dns_json["uuid"]] - - -class TestJSONSIEMFriendly(ModuleTestBase): - modules_overrides = ["json"] - config_overrides = {"modules": {"json": {"siem_friendly": True}}} - - def check(self, module_test, events): - txt_file = module_test.scan.home / "output.json" - lines = list(module_test.scan.helpers.read_file(txt_file)) - passed = False - for line in lines: - e = json.loads(line) - if e["data"] == {"DNS_NAME": "blacklanternsecurity.com"}: - passed = True - assert passed diff --git a/bbot/test/test_step_2/module_tests/test_module_kafka.py b/bbot/test/test_step_2/module_tests/test_module_kafka.py new file mode 100644 index 0000000000..451a551e5f --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_kafka.py @@ -0,0 +1,99 @@ +import json +import asyncio + +from .base import ModuleTestBase + + +class TestKafka(ModuleTestBase): + config_overrides = { + "modules": { + "kafka": { + "bootstrap_servers": "localhost:9092", + "topic": "bbot_events", + } + } + } + skip_distro_tests = True + + async def setup_before_prep(self, module_test): + # Start Zookeeper + await asyncio.create_subprocess_exec( + "docker", "run", "-d", "--rm", "--name", "bbot-test-zookeeper", "-p", "2181:2181", "zookeeper:3.9" + ) + + # Wait for Zookeeper to be ready + await self.wait_for_port_open(2181) + + # Start Kafka using wurstmeister/kafka + await asyncio.create_subprocess_exec( + "docker", + "run", + "-d", + "--rm", + "--name", + "bbot-test-kafka", + "--link", + "bbot-test-zookeeper:zookeeper", + "-e", + "KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181", + "-e", + "KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092", + "-e", + "KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092", + "-e", + "KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1", + "-p", + "9092:9092", + "wurstmeister/kafka", + ) + + # Wait for Kafka to be ready + await self.wait_for_port_open(9092) + + await asyncio.sleep(1) + + async def check(self, module_test, events): + from aiokafka import AIOKafkaConsumer + + self.consumer = AIOKafkaConsumer( + "bbot_events", + bootstrap_servers="localhost:9092", + group_id="test_group", + auto_offset_reset="earliest", + ) + await self.consumer.start() + + try: + events_json = [e.json() for e in events] + events_json.sort(key=lambda x: x["timestamp"]) + + # Collect events from Kafka with a timeout to prevent CI hangs + kafka_events = [] + + async def _consume(): + async for msg in self.consumer: + event_data = json.loads(msg.value.decode("utf-8")) + kafka_events.append(event_data) + if len(kafka_events) >= len(events_json): + break + + await asyncio.wait_for(_consume(), timeout=30) + + kafka_events.sort(key=lambda x: x["timestamp"]) + + # Verify the events match + assert events_json == kafka_events, "Events do not match" + + finally: + # Clean up: Stop the Kafka consumer + if hasattr(self, "consumer") and not self.consumer._closed: + await self.consumer.stop() + # Stop Kafka and Zookeeper containers + p1 = await asyncio.create_subprocess_exec( + "docker", "stop", "bbot-test-kafka", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + await p1.communicate() + p2 = await asyncio.create_subprocess_exec( + "docker", "stop", "bbot-test-zookeeper", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + await p2.communicate() diff --git a/bbot/test/test_step_2/module_tests/test_module_kreuzberg.py b/bbot/test/test_step_2/module_tests/test_module_kreuzberg.py new file mode 100644 index 0000000000..217375dbd5 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_kreuzberg.py @@ -0,0 +1,89 @@ +import base64 +from pathlib import Path +from .base import ModuleTestBase + +from ...bbot_fixtures import * + + +class TestKreuzberg(ModuleTestBase): + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["kreuzberg", "filedownload", "httpx", "excavate", "speculate"] + config_overrides = { + "web": { + "spider_distance": 2, + "spider_depth": 2, + }, + "modules": { + "filedownload": { + "output_folder": bbot_test_dir / "filedownload", + }, + }, + } + + pdf_data = base64.b64decode( + "JVBERi0xLjMKJe+/ve+/ve+/ve+/vSBSZXBvcnRMYWIgR2VuZXJhdGVkIFBERiBkb2N1bWVudCBodHRwOi8vd3d3LnJlcG9ydGxhYi5jb20KMSAwIG9iago8PAovRjEgMiAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL0Jhc2VGb250IC9IZWx2ZXRpY2EgL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcgL05hbWUgL0YxIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAvRm9udAo+PgplbmRvYmoKMyAwIG9iago8PAovQ29udGVudHMgNyAwIFIgL01lZGlhQm94IFsgMCAwIDU5NS4yNzU2IDg0MS44ODk4IF0gL1BhcmVudCA2IDAgUiAvUmVzb3VyY2VzIDw8Ci9Gb250IDEgMCBSIC9Qcm9jU2V0IFsgL1BERiAvVGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSSBdCj4+IC9Sb3RhdGUgMCAvVHJhbnMgPDwKCj4+IAogIC9UeXBlIC9QYWdlCj4+CmVuZG9iago0IDAgb2JqCjw8Ci9QYWdlTW9kZSAvVXNlTm9uZSAvUGFnZXMgNiAwIFIgL1R5cGUgL0NhdGFsb2cKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL0F1dGhvciAoYW5vbnltb3VzKSAvQ3JlYXRpb25EYXRlIChEOjIwMjQwNjAzMTg1ODE2KzAwJzAwJykgL0NyZWF0b3IgKFJlcG9ydExhYiBQREYgTGlicmFyeSAtIHd3dy5yZXBvcnRsYWIuY29tKSAvS2V5d29yZHMgKCkgL01vZERhdGUgKEQ6MjAyNDA2MDMxODU4MTYrMDAnMDAnKSAvUHJvZHVjZXIgKFJlcG9ydExhYiBQREYgTGlicmFyeSAtIHd3dy5yZXBvcnRsYWIuY29tKSAKICAvU3ViamVjdCAodW5zcGVjaWZpZWQpIC9UaXRsZSAodW50aXRsZWQpIC9UcmFwcGVkIC9GYWxzZQo+PgplbmRvYmoKNiAwIG9iago8PAovQ291bnQgMSAvS2lkcyBbIDMgMCBSIF0gL1R5cGUgL1BhZ2VzCj4+CmVuZG9iago3IDAgb2JqCjw8Ci9GaWx0ZXIgWyAvQVNDSUk4NURlY29kZSAvRmxhdGVEZWNvZGUgXSAvTGVuZ3RoIDEwNwo+PgpzdHJlYW0KR2FwUWgwRT1GLDBVXEgzVFxwTllUXlFLaz90Yz5JUCw7VyNVMV4yM2loUEVNXz9DVzRLSVNpOTBNakdeMixGUyM8UkM1K2MsbilaOyRiSyRiIjVJWzwhXlREI2dpXSY9NVgsWzVAWUBWfj5lbmRzdHJlYW0KZW5kb2JqCnhyZWYKMCA4CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDA3MyAwMDAwMCBuIAowMDAwMDAwMTA0IDAwMDAwIG4gCjAwMDAwMDAyMTEgMDAwMDAgbiAKMDAwMDAwMDQxNCAwMDAwMCBuIAowMDAwMDAwNDgyIDAwMDAwIG4gCjAwMDAwMDA3NzggMDAwMDAgbiAKMDAwMDAwMDgzNyAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9JRCAKWzw4MGQ5ZjViOTY0ZmM5OTI4NDUwMWRlYjdhNmE2MzdmNz48ODBkOWY1Yjk2NGZjOTkyODQ1MDFkZWI3YTZhNjM3Zjc+XQolIFJlcG9ydExhYiBnZW5lcmF0ZWQgUERGIGRvY3VtZW50IC0tIGRpZ2VzdCAoaHR0cDovL3d3dy5yZXBvcnRsYWIuY29tKQoKL0luZm8gNSAwIFIKL1Jvb3QgNCAwIFIKL1NpemUgOAo+PgpzdGFydHhyZWYKMTAzNAolJUVPRg==" + ) + + docx_data = base64.b64decode( + "UEsDBBQAAAAAAFitWVzXeYTquAEAALgBAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbDw/eG1sIHZl" + "cnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4KPFR5cGVzIHht" + "bG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvcGFja2FnZS8yMDA2L2NvbnRl" + "bnQtdHlwZXMiPgogIDxEZWZhdWx0IEV4dGVuc2lvbj0icmVscyIgQ29udGVudFR5cGU9ImFwcGxp" + "Y2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1wYWNrYWdlLnJlbGF0aW9uc2hpcHMreG1sIi8+CiAg" + "PERlZmF1bHQgRXh0ZW5zaW9uPSJ4bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi94bWwiLz4K" + "ICA8T3ZlcnJpZGUgUGFydE5hbWU9Ii93b3JkL2RvY3VtZW50LnhtbCIgQ29udGVudFR5cGU9ImFw" + "cGxpY2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1vZmZpY2Vkb2N1bWVudC53b3JkcHJvY2Vzc2lu" + "Z21sLmRvY3VtZW50Lm1haW4reG1sIi8+CjwvVHlwZXM+UEsDBBQAAAAAAFitWVwgG4bqLgEAAC4B" + "AAALAAAAX3JlbHMvLnJlbHM8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3Rh" + "bmRhbG9uZT0ieWVzIj8+CjxSZWxhdGlvbnNoaXBzIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVu" + "eG1sZm9ybWF0cy5vcmcvcGFja2FnZS8yMDA2L3JlbGF0aW9uc2hpcHMiPgogIDxSZWxhdGlvbnNo" + "aXAgSWQ9InJJZDEiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZp" + "Y2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMvb2ZmaWNlRG9jdW1lbnQiIFRhcmdldD0id29y" + "ZC9kb2N1bWVudC54bWwiLz4KPC9SZWxhdGlvbnNoaXBzPlBLAwQUAAAAAABYrVlcNNIntwABAAAA" + "AQAAEQAAAHdvcmQvZG9jdW1lbnQueG1sPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRG" + "LTgiIHN0YW5kYWxvbmU9InllcyI/Pgo8dzpkb2N1bWVudCB4bWxuczp3PSJodHRwOi8vc2NoZW1h" + "cy5vcGVueG1sZm9ybWF0cy5vcmcvd29yZHByb2Nlc3NpbmdtbC8yMDA2L21haW4iPgogIDx3OmJv" + "ZHk+CiAgICA8dzpwPgogICAgICA8dzpyPgogICAgICAgIDx3OnQ+SGVsbG8sIFdvcmxkISE8L3c6" + "dD4KICAgICAgPC93OnI+CiAgICA8L3c6cD4KICA8L3c6Ym9keT4KPC93OmRvY3VtZW50PlBLAQIU" + "AxQAAAAAAFitWVzXeYTquAEAALgBAAATAAAAAAAAAAAAAACAAQAAAABbQ29udGVudF9UeXBlc10u" + "eG1sUEsBAhQDFAAAAAAAWK1ZXCAbhuouAQAALgEAAAsAAAAAAAAAAAAAAIAB6QEAAF9yZWxzLy5y" + "ZWxzUEsBAhQDFAAAAAAAWK1ZXDTSJ7cAAQAAAAEAABEAAAAAAAAAAAAAAIABQAMAAHdvcmQvZG9j" + "dW1lbnQueG1sUEsFBgAAAAADAAMAuQAAAG8EAAAAAA==" + ) + + expected_result_pdf = "Hello, World!" + expected_result_docx = "Hello, World!!" + + async def setup_after_prep(self, module_test): + module_test.set_expect_requests( + {"uri": "/"}, + {"response_data": ''}, + ) + module_test.set_expect_requests( + {"uri": "/Test_PDF"}, + {"response_data": self.pdf_data, "headers": {"Content-Type": "application/pdf"}}, + ) + module_test.set_expect_requests( + {"uri": "/Test_DOCX"}, + { + "response_data": self.docx_data, + "headers": {"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + }, + ) + + def check(self, module_test, events): + filesystem_events = [e for e in events if e.type == "FILESYSTEM"] + assert 2 == len(filesystem_events), filesystem_events + for filesystem_event in filesystem_events: + file = Path(filesystem_event.data["path"]) + assert file.is_file(), "Destination file doesn't exist" + assert open(file, "rb").read() == self.pdf_data or open(file, "rb").read() == self.docx_data, ( + f"File at {file} does not contain the correct content" + ) + raw_text_events = [e for e in events if e.type == "RAW_TEXT"] + assert 2 == len(raw_text_events), "Failed to emit RAW_TEXT event" + for raw_text_event in raw_text_events: + assert raw_text_event.data in [ + self.expected_result_pdf, + self.expected_result_docx, + ], f"Text extracted from {raw_text_event.data['path']} is incorrect, got {raw_text_event.data}" diff --git a/bbot/test/test_step_2/module_tests/test_module_lightfuzz.py b/bbot/test/test_step_2/module_tests/test_module_lightfuzz.py index 211857619a..b734a58f60 100644 --- a/bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +++ b/bbot/test/test_step_2/module_tests/test_module_lightfuzz.py @@ -1177,6 +1177,9 @@ def check(self, module_test, events): lightfuzz_serial_detect_errorresolution = False for e in events: + print("@@@@") + print(e.type) + print(e.data) if e.type == "WEB_PARAMETER": if e.data["name"] == "TextBox1": excavate_extracted_form_parameter = True @@ -1328,6 +1331,60 @@ def check(self, module_test, events): assert finding_count == 0, "Unexpected FINDING events reported" +# Serialization Module (Error Resolution - Transient Baseline) +# Simulates a server that returns 500 on the first request (baseline), then 200 for everything after. +# The confirmation re-send of the control payload should catch this and suppress the finding. +class Test_Lightfuzz_serial_errorresolution_transient_baseline(Test_Lightfuzz_serial_errorresolution): + request_count = 0 + + def request_handler(self, request): + post_params = request.form + + if "TextBox1" not in post_params.keys(): + return Response(self.dotnet_serial_html, status=200) + + self.request_count += 1 + # First request (baseline) returns 500, all subsequent requests return 200 + if self.request_count <= 1: + return Response(self.dotnet_serial_error, status=500) + else: + return Response("OK", status=200) + + def check(self, module_test, events): + no_finding_emitted = True + for e in events: + if e.type == "FINDING" and "Error Resolution" in e.data.get("description", ""): + no_finding_emitted = False + assert no_finding_emitted, "False positive Error Resolution finding was emitted despite transient baseline" + + +# Serialization Module (Error Resolution - Multi-Language Family False Positive) +# Simulates a server where ALL serialization payloads resolve the error (500->200), +# spanning multiple language families. The multi-family check should discard them all. +class Test_Lightfuzz_serial_errorresolution_multi_language(Test_Lightfuzz_serial_errorresolution): + def request_handler(self, request): + post_params = request.form + + if "TextBox1" not in post_params.keys(): + return Response(self.dotnet_serial_html, status=200) + + # __VIEWSTATE mismatch triggers the baseline path + if post_params["__VIEWSTATE"] != "/wEPDwULLTE5MTI4MzkxNjVkZNt7ICM+GixNryV6ucx+srzhXlwP": + return Response(self.dotnet_serial_error, status=500) + + # ALL payloads "resolve" the error - this is the false positive scenario + return Response("OK", status=200) + + def check(self, module_test, events): + no_finding_emitted = True + for e in events: + if e.type == "FINDING" and "Error Resolution" in e.data.get("description", ""): + no_finding_emitted = False + assert no_finding_emitted, ( + "False positive Error Resolution finding was emitted despite multiple language families triggering" + ) + + # CMDi echo canary class Test_Lightfuzz_cmdi(ModuleTestBase): targets = ["http://127.0.0.1:8888"] @@ -1435,9 +1492,14 @@ def request_handler(self, request): async def setup_before_prep(self, module_test): self.interactsh_mock_instance = module_test.mock_interactsh("lightfuzz") - module_test.monkeypatch.setattr( - module_test.scan.helpers, "interactsh", lambda *args, **kwargs: self.interactsh_mock_instance - ) + # Mock at the helper creation level BEFORE modules are set up + def mock_interactsh_factory(*args, **kwargs): + return self.interactsh_mock_instance + + # Apply the mock to the core helpers so modules get the mock during setup + from bbot.core.helpers.helper import ConfigAwareHelper + + module_test.monkeypatch.setattr(ConfigAwareHelper, "interactsh", mock_interactsh_factory) async def setup_after_prep(self, module_test): expect_args = re.compile("/") @@ -1451,7 +1513,7 @@ def check(self, module_test, events): if "HTTP Extracted Parameter [search]" in e.data["description"]: web_parameter_emitted = True - if e.type == "VULNERABILITY": + if e.type == "FINDING": if ( "OS Command Injection (OOB Interaction) Type: [GETPARAM] Parameter Name: [search] Probe: [&&]" in e.data["description"] @@ -1462,6 +1524,91 @@ def check(self, module_test, events): assert cmdi_interacttsh_finding_emitted, "interactsh CMDi FINDING not emitted" +# SSRF interactsh +class Test_Lightfuzz_ssrf(ModuleTestBase): + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["httpx", "lightfuzz", "excavate"] + + @staticmethod + def extract_subdomain_tag(data): + # Try both URL-encoded and non-encoded forms + for pattern in [ + r"url=https?%3A%2F%2F(.+?)\.fakedomain\.fakeinteractsh\.com", + r"url=https?://(.+?)\.fakedomain\.fakeinteractsh\.com", + ]: + match = re.search(pattern, data) + if match: + return match.group(1) + + config_overrides = { + "interactsh_disable": False, + "modules": { + "lightfuzz": { + "enabled_submodules": ["ssrf"], + } + }, + } + + def request_handler(self, request): + qs = str(request.query_string.decode()) + + parameter_block = """ + + """ + + if "url=" in qs: + subdomain_tag = self.extract_subdomain_tag(request.full_path) + + if subdomain_tag: + self.interactsh_mock_instance.mock_interaction(subdomain_tag) + return Response(parameter_block, status=200) + + async def setup_before_prep(self, module_test): + self.interactsh_mock_instance = module_test.mock_interactsh("lightfuzz") + + module_test.monkeypatch.setattr( + module_test.scan.helpers, "interactsh", lambda *args, **kwargs: self.interactsh_mock_instance + ) + + async def setup_after_prep(self, module_test): + expect_args = re.compile("/") + module_test.set_expect_requests_handler(expect_args=expect_args, request_handler=self.request_handler) + + def check(self, module_test, events): + web_parameter_emitted = False + ssrf_dns_finding_emitted = False + ssrf_http_finding_emitted = False + for e in events: + if e.type == "WEB_PARAMETER": + if "HTTP Extracted Parameter [url]" in e.data["description"]: + web_parameter_emitted = True + + if e.type == "FINDING": + if ( + "Server-Side Request Forgery (OOB Interaction) Type: [GETPARAM] Parameter Name: [url]" + in e.data["description"] + ): + if "Interaction Protocol: [dns]" in e.data["description"]: + ssrf_dns_finding_emitted = True + assert e.data["confidence"] == "MEDIUM", ( + f"DNS SSRF should be MEDIUM, got {e.data['confidence']}" + ) + elif "Interaction Protocol: [http]" in e.data["description"]: + ssrf_http_finding_emitted = True + assert e.data["confidence"] == "CONFIRMED", ( + f"HTTP SSRF should be CONFIRMED, got {e.data['confidence']}" + ) + + assert web_parameter_emitted, "WEB_PARAMETER was not emitted" + assert ssrf_dns_finding_emitted, "interactsh SSRF DNS FINDING not emitted" + assert ssrf_http_finding_emitted, "interactsh SSRF HTTP FINDING not emitted" + + class Test_Lightfuzz_speculative(ModuleTestBase): targets = ["http://127.0.0.1:8888/"] modules_overrides = ["httpx", "excavate", "paramminer_getparams", "lightfuzz"] @@ -1678,8 +1825,6 @@ def check(self, module_test, events): == "Probable Cryptographic Parameter. Parameter: [encrypted_data] Parameter Type: [POSTPARAM] Original Value: [dplyorsu8VUriMW/8DqVDU6kRwL/FDk3Q%2B4GXVGZbo0CTh9YX1YvzZZJrYe4cHxvAICyliYtp1im4fWoOa54Zg%3D%3D] Detection Technique(s): [Single-byte Mutation] Envelopes: [URL-Encoded]" ): cryptographic_parameter_finding = True - - if e.type == "VULNERABILITY": if ( e.data["description"] == "Padding Oracle Vulnerability. Block size: [16] Parameter: [encrypted_data] Parameter Type: [POSTPARAM] Original Value: [dplyorsu8VUriMW/8DqVDU6kRwL/FDk3Q%2B4GXVGZbo0CTh9YX1YvzZZJrYe4cHxvAICyliYtp1im4fWoOa54Zg%3D%3D] Envelopes: [URL-Encoded]" @@ -1741,7 +1886,7 @@ def check(self, module_test, events): ): cryptographic_parameter_finding = True - if e.type == "VULNERABILITY": + if e.type == "FINDING": if ( "Padding Oracle Vulnerability. Block size: [16]" in e.data["description"] and "encrypted_data" in e.data["description"] @@ -1755,7 +1900,7 @@ def check(self, module_test, events): class Test_Lightfuzz_PaddingOracleDetection_Noisy(Test_Lightfuzz_PaddingOracleDetection): """Padding oracle negative test: the server returns different responses for ~30 byte values, - which exceeds any valid block size. This should NOT produce a VULNERABILITY.""" + which exceeds any valid block size. This should NOT produce a FINDING.""" def request_handler(self, request): encrypted_value = quote( @@ -1817,7 +1962,7 @@ def check(self, module_test, events): and "encrypted_data" in e.data["description"] ): cryptographic_parameter_finding = True - if e.type == "VULNERABILITY": + if e.type == "FINDING": if "Padding Oracle" in e.data["description"]: padding_oracle_detected = True @@ -1828,6 +1973,137 @@ def check(self, module_test, events): ) +class Test_Lightfuzz_PaddingOracleDetection_NarrowCharset(ModuleTestBase): + """Parameters with values that use a narrow consecutive character set (e.g. only A-P) + round-trip as valid base64 but are not actual cryptographic data. + The narrow charset check should reject these, preventing false findings.""" + + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["httpx", "excavate", "lightfuzz"] + config_overrides = { + "interactsh_disable": True, + "modules": { + "lightfuzz": { + "enabled_submodules": ["crypto"], + } + }, + } + + # A-P only value: valid base64 round-trip, but narrow charset (span=15) + narrow_charset_value = "BPIDFOPFGNLIBNFGAEPBMIIKPHEFGOJKOMACMBJJLOPKFIGMKALJDBHCMAMHIKIMDOFDHIBAHEJBIIGMIDKANCMFGJAIEKLPCLFDMELEAGBILMHLAPFKNNBAMPPNDEEP" + + def request_handler(self, request): + return Response("

Welcome

", status=200) + + async def setup_after_prep(self, module_test): + module_test.set_expect_requests_handler(expect_args=re.compile(".*"), request_handler=self.request_handler) + + parent_event = module_test.scan.make_event( + "http://127.0.0.1:8888/", + "URL", + module_test.scan.root_event, + module="httpx", + tags=["status-200", "distance-0"], + ) + + data = { + "host": "127.0.0.1", + "type": "COOKIE", + "name": "custom_session_cookie", + "original_value": self.narrow_charset_value, + "url": "http://127.0.0.1:8888/", + "description": "Test narrow charset cookie", + } + seed_event = module_test.scan.make_event(data, "WEB_PARAMETER", parent_event, tags=["distance-0"]) + await module_test.scan.ingress_module.incoming_event_queue.put(seed_event) + + def check(self, module_test, events): + crypto_finding = False + padding_oracle_finding = False + for e in events: + if e.type == "FINDING": + if "Cryptographic Parameter" in e.data["description"]: + crypto_finding = True + if "Padding Oracle" in e.data["description"]: + padding_oracle_finding = True + + assert not crypto_finding, "Narrow-charset parameter should NOT be identified as cryptographic" + assert not padding_oracle_finding, "Narrow-charset parameter should NOT trigger padding oracle detection" + + +class Test_Lightfuzz_PaddingOracleDetection_Jitter(Test_Lightfuzz_PaddingOracleDetection): + """Padding oracle negative test: the server produces inconsistent responses across rounds. + The first round may trigger detection, but the confirmation round should fail, + suppressing the jitter-based false positive.""" + + oracle_request_count = 0 + + def request_handler(self, request): + encrypted_value = quote( + "dplyorsu8VUriMW/8DqVDU6kRwL/FDk3Q+4GXVGZbo0CTh9YX1YvzZZJrYe4cHxvAICyliYtp1im4fWoOa54Zg==" + ) + default_html_response = f""" + + +
+ + +
+ + + """ + + if "/decrypt" in request.url and request.method == "POST": + if request.form and request.form["encrypted_data"]: + encrypted_data = request.form["encrypted_data"] + + if "4GXVGZbo0DTh9YX1YvzZZJrYe4cHxvAICyliYtp1im4fWoOa54Zg" in encrypted_data: + response_content = "DIFFERENT CRYPTOGRAPHIC ERROR" + elif encrypted_data.startswith("AAAAAAAAAAAAAAAA"): + Test_Lightfuzz_PaddingOracleDetection_Jitter.oracle_request_count += 1 + # First 254 requests (round 1): produce oracle-like signal (1 differ) + if Test_Lightfuzz_PaddingOracleDetection_Jitter.oracle_request_count <= 254: + try: + decoded = base64.b64decode(encrypted_data) + if len(decoded) >= 32: + varying_byte = decoded[31] + if varying_byte == 100: + response_content = "Padding error detected" + else: + response_content = "Decryption failed" + else: + response_content = "Decryption failed" + except Exception: + response_content = "Decryption failed" + else: + # Confirmation round: all responses identical (no oracle signal) + response_content = "Decryption failed" + elif "AAAAAAA" in encrypted_data: + response_content = "YET DIFFERENT CRYPTOGRAPHIC ERROR" + else: + response_content = "Decryption failed" + + return Response(response_content, status=200) + else: + return Response(default_html_response, status=200) + + def check(self, module_test, events): + web_parameter_extracted = False + padding_oracle_detected = False + for e in events: + if e.type == "WEB_PARAMETER": + if "HTTP Extracted Parameter [encrypted_data] (POST Form" in e.data["description"]: + web_parameter_extracted = True + if e.type == "FINDING": + if "Padding Oracle" in e.data["description"]: + padding_oracle_detected = True + + assert web_parameter_extracted, "Web parameter was not extracted" + assert not padding_oracle_detected, ( + "Padding oracle should NOT be detected when confirmation round fails (jitter false positive)" + ) + + class Test_Lightfuzz_XSS_jsquotecontext(ModuleTestBase): targets = ["http://127.0.0.1:8888"] modules_overrides = ["httpx", "lightfuzz", "excavate", "paramminer_getparams"] diff --git a/bbot/test/test_step_2/module_tests/test_module_medusa.py b/bbot/test/test_step_2/module_tests/test_module_medusa.py index 52743ebd5c..773c6733b5 100644 --- a/bbot/test/test_step_2/module_tests/test_module_medusa.py +++ b/bbot/test/test_step_2/module_tests/test_module_medusa.py @@ -44,7 +44,9 @@ async def setup_after_prep(self, module_test): await module_test.module.emit_event(protocol_event) def check(self, module_test, events): - vuln_events = [e for e in events if e.type == "VULNERABILITY"] + vuln_events = [e for e in events if e.type == "FINDING"] assert len(vuln_events) == 1 assert "VALID [SNMPV2C] CREDENTIALS FOUND: public [READ]" in vuln_events[0].data["description"] + assert vuln_events[0].data["severity"] == "CRITICAL" + assert vuln_events[0].data["confidence"] == "CONFIRMED" diff --git a/bbot/test/test_step_2/module_tests/test_module_mongo.py b/bbot/test/test_step_2/module_tests/test_module_mongo.py new file mode 100644 index 0000000000..67a612c33c --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_mongo.py @@ -0,0 +1,152 @@ +import asyncio + +from .base import ModuleTestBase + + +class TestMongo(ModuleTestBase): + test_db_name = "bbot_test" + test_collection_prefix = "test_" + config_overrides = { + "modules": { + "mongo": { + "database": test_db_name, + "username": "bbot", + "password": "bbotislife", + "collection_prefix": test_collection_prefix, + } + } + } + skip_distro_tests = True + + async def setup_before_prep(self, module_test): + await asyncio.create_subprocess_exec( + "docker", + "run", + "--name", + "bbot-test-mongo", + "--rm", + "-e", + "MONGO_INITDB_ROOT_USERNAME=bbot", + "-e", + "MONGO_INITDB_ROOT_PASSWORD=bbotislife", + "-p", + "27017:27017", + "-d", + "mongo", + ) + + from pymongo import AsyncMongoClient + + # Connect to the MongoDB collection with retry logic + while True: + try: + client = AsyncMongoClient("mongodb://localhost:27017", username="bbot", password="bbotislife") + db = client[self.test_db_name] + events_collection = db.get_collection(self.test_collection_prefix + "events") + # Attempt a simple operation to confirm the connection + await events_collection.count_documents({}) + break # Exit the loop if connection is successful + except Exception as e: + print(f"Connection failed: {e}. Retrying...") + await asyncio.sleep(0.5) + + # Check that there are no events in the collection + count = await events_collection.count_documents({}) + assert count == 0, "There are existing events in the database" + + # Close the MongoDB connection + await client.aclose() + + async def check(self, module_test, events): + try: + from bbot.models.pydantic import Event + from pymongo import AsyncMongoClient + + events_json = [e.json() for e in events] + events_json.sort(key=lambda x: x["timestamp"]) + + # Connect to the MongoDB collection + client = AsyncMongoClient("mongodb://localhost:27017", username="bbot", password="bbotislife") + db = client[self.test_db_name] + events_collection = db.get_collection(self.test_collection_prefix + "events") + + ### INDEXES ### + + # make sure the collection has all the right indexes + indexes_cursor = await events_collection.list_indexes() + indexes = await indexes_cursor.to_list(length=None) + # indexes = await cursor.to_list(length=None) + for field in Event.indexed_fields(): + assert any(field in index["key"] for index in indexes), f"Index for {field} not found" + + ### EVENTS ### + + # Fetch all events from the collection + cursor = events_collection.find({}) + db_events = await cursor.to_list(length=None) + + # make sure we have the same number of events + assert len(events_json) == len(db_events) + + for db_event in db_events: + assert isinstance(db_event["timestamp"], float) + assert isinstance(db_event["inserted_at"], float) + + # Convert to Pydantic objects and dump them + db_events_pydantic = [Event(**e).model_dump(exclude_none=True) for e in db_events] + db_events_pydantic.sort(key=lambda x: x["timestamp"]) + + # Find the main event with type DNS_NAME and data blacklanternsecurity.com + main_event = next( + ( + e + for e in db_events_pydantic + if e.get("type") == "DNS_NAME" and e.get("data") == "blacklanternsecurity.com" + ), + None, + ) + assert main_event is not None, "Main event with type DNS_NAME and data blacklanternsecurity.com not found" + + # Ensure it has the reverse_host attribute + expected_reverse_host = "blacklanternsecurity.com"[::-1] + assert main_event.get("reverse_host") == expected_reverse_host, ( + f"reverse_host attribute is not correct, expected {expected_reverse_host}" + ) + + # Events don't match exactly because the mongo ones have reverse_host and inserted_at + assert events_json != db_events_pydantic + for db_event in db_events_pydantic: + db_event.pop("reverse_host", None) + db_event.pop("inserted_at", None) + db_event.pop("archived", None) + # They should match after removing reverse_host + assert events_json == db_events_pydantic, "Events do not match" + + ### SCANS ### + + # Fetch all scans from the collection + cursor = db.get_collection(self.test_collection_prefix + "scans").find({}) + db_scans = await cursor.to_list(length=None) + assert len(db_scans) == 1, "There should be exactly one scan" + db_scan = db_scans[0] + assert db_scan["id"] == main_event["scan"], "Scan id should match main event scan" + + ### TARGETS ### + + # Fetch all targets from the collection + cursor = db.get_collection(self.test_collection_prefix + "targets").find({}) + db_targets = await cursor.to_list(length=None) + assert len(db_targets) == 1, "There should be exactly one target" + db_target = db_targets[0] + scan_event = next(e for e in events if e.type == "SCAN") + assert db_target["hash"] == scan_event.data["target"]["hash"], "Target hash should match scan target hash" + + finally: + # Clean up: Delete all documents in the collection + await events_collection.delete_many({}) + # Close the MongoDB connection + await client.aclose() + process = await asyncio.create_subprocess_exec( + "docker", "stop", "bbot-test-mongo", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + await process.communicate() diff --git a/bbot/test/test_step_2/module_tests/test_module_mysql.py b/bbot/test/test_step_2/module_tests/test_module_mysql.py index 4867c568d5..de30c58f9f 100644 --- a/bbot/test/test_step_2/module_tests/test_module_mysql.py +++ b/bbot/test/test_step_2/module_tests/test_module_mysql.py @@ -1,5 +1,4 @@ import asyncio -import time from .base import ModuleTestBase @@ -28,20 +27,8 @@ async def setup_before_prep(self, module_test): ) stdout, stderr = await process.communicate() - import aiomysql - # wait for the container to start - start_time = time.time() - while True: - try: - conn = await aiomysql.connect(user="root", password="bbotislife", db="bbot", host="localhost") - conn.close() - break - except Exception as e: - if time.time() - start_time > 60: # timeout after 60 seconds - self.log.error("MySQL server did not start in time.") - raise e - await asyncio.sleep(1) + await self.wait_for_port_open(3306) if process.returncode != 0: self.log.error(f"Failed to start MySQL server: {stderr.decode()}") diff --git a/bbot/test/test_step_2/module_tests/test_module_nats.py b/bbot/test/test_step_2/module_tests/test_module_nats.py new file mode 100644 index 0000000000..32e37a1ee8 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_nats.py @@ -0,0 +1,66 @@ +import json +import asyncio +from contextlib import suppress + +from .base import ModuleTestBase + + +class TestNats(ModuleTestBase): + config_overrides = { + "modules": { + "nats": { + "servers": ["nats://localhost:4222"], + "subject": "bbot_events", + } + } + } + skip_distro_tests = True + + async def setup_before_prep(self, module_test): + # Start NATS server + await asyncio.create_subprocess_exec( + "docker", "run", "-d", "--rm", "--name", "bbot-test-nats", "-p", "4222:4222", "nats:latest" + ) + + # Wait for NATS to be ready by checking the port + await self.wait_for_port_open(4222) + + # Connect to NATS + import nats + + try: + self.nc = await nats.connect(["nats://localhost:4222"]) + except Exception as e: + self.log.error(f"Error connecting to NATS: {e}") + raise + + # Collect events from NATS + self.nats_events = [] + + async def message_handler(msg): + event_data = json.loads(msg.data.decode("utf-8")) + self.nats_events.append(event_data) + + await self.nc.subscribe("bbot_events", cb=message_handler) + + async def check(self, module_test, events): + try: + events_json = [e.json() for e in events] + events_json.sort(key=lambda x: x["timestamp"]) + + self.nats_events.sort(key=lambda x: x["timestamp"]) + + # Verify the events match + assert events_json == self.nats_events, "Events do not match" + + finally: + with suppress(Exception): + # Clean up: Stop the NATS client + if self.nc.is_connected: + await self.nc.drain() + await self.nc.close() + # Stop NATS server container + process = await asyncio.create_subprocess_exec( + "docker", "stop", "bbot-test-nats", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + await process.communicate() diff --git a/bbot/test/test_step_2/module_tests/test_module_neo4j.py b/bbot/test/test_step_2/module_tests/test_module_neo4j.py index c5df1e4748..be395206d3 100644 --- a/bbot/test/test_step_2/module_tests/test_module_neo4j.py +++ b/bbot/test/test_step_2/module_tests/test_module_neo4j.py @@ -4,13 +4,14 @@ class TestNeo4j(ModuleTestBase): config_overrides = {"modules": {"neo4j": {"uri": "bolt://127.0.0.1:11111"}}} - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): # install neo4j deps_pip = module_test.preloaded["neo4j"]["deps"]["pip"] await module_test.scan.helpers.depsinstaller.pip_install(deps_pip) self.neo4j_used = False + async def setup_before_prep(self, module_test): class MockResult: async def data(s): self.neo4j_used = True diff --git a/bbot/test/test_step_2/module_tests/test_module_nmap_xml.py b/bbot/test/test_step_2/module_tests/test_module_nmap_xml.py index b88595be01..644cd4e928 100644 --- a/bbot/test/test_step_2/module_tests/test_module_nmap_xml.py +++ b/bbot/test/test_step_2/module_tests/test_module_nmap_xml.py @@ -25,9 +25,17 @@ async def handle_event(self, event): {"host": str(event.host), "port": event.port, "protocol": "https"}, "PROTOCOL", parent=event ) - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): self.dummy_module = self.DummyModule(module_test.scan) module_test.scan.modules["dummy_module"] = self.dummy_module + await self.dummy_module.setup() + + # Manually update speculate module's open_port_consumers setting + speculate_module = module_test.scan.modules.get("speculate") + if speculate_module: + speculate_module.open_port_consumers = True + speculate_module.emit_open_ports = True + await module_test.mock_dns( { "blacklanternsecurity.com": {"A": ["127.0.0.1", "127.0.0.2"]}, diff --git a/bbot/test/test_step_2/module_tests/test_module_nuclei.py b/bbot/test/test_step_2/module_tests/test_module_nuclei.py index cc19e0da77..65a363a390 100644 --- a/bbot/test/test_step_2/module_tests/test_module_nuclei.py +++ b/bbot/test/test_step_2/module_tests/test_module_nuclei.py @@ -53,8 +53,11 @@ def check(self, module_test, events): if e.type == "FINDING": if "Directory listing enabled" in e.data["description"]: first_run_detect = True + # Nuclei emits HIGH confidence for most findings + assert e.data["confidence"] == "HIGH" elif "Copyright" in e.data["description"]: second_run_detect = True + assert e.data["confidence"] == "HIGH" assert first_run_detect assert second_run_detect @@ -82,9 +85,7 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - assert any( - e.type == "VULNERABILITY" and "Generic Env File Disclosure" in e.data["description"] for e in events - ) + assert any(e.type == "FINDING" and "Generic Env File Disclosure" in e.data["description"] for e in events) class TestNucleiTechnology(TestNucleiManual): diff --git a/bbot/test/test_step_2/module_tests/test_module_oauth.py b/bbot/test/test_step_2/module_tests/test_module_oauth.py index 1e7078e840..99e19dccca 100644 --- a/bbot/test/test_step_2/module_tests/test_module_oauth.py +++ b/bbot/test/test_step_2/module_tests/test_module_oauth.py @@ -1,12 +1,10 @@ from .base import ModuleTestBase -from .test_module_azure_realm import TestAzure_Realm as Azure_Realm - class TestOAUTH(ModuleTestBase): targets = ["evilcorp.com"] config_overrides = {"scope": {"report_distance": 1}, "omit_event_types": []} - modules_overrides = ["azure_realm", "oauth"] + modules_overrides = ["azure_tenant", "oauth"] openid_config_azure = { "token_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/token", "token_endpoint_auth_methods_supported": ["client_secret_post", "private_key_jwt", "client_secret_basic"], @@ -166,10 +164,44 @@ class TestOAUTH(ModuleTestBase): async def setup_after_prep(self, module_test): await module_test.mock_dns({"evilcorp.com": {"A": ["127.0.0.1"]}}) + + # azure_tenant mocks module_test.httpx_mock.add_response( - url="https://login.microsoftonline.com/getuserrealm.srf?login=test@evilcorp.com", - json=Azure_Realm.response_json, + url="https://azmap.dev/api/tenant?domain=evilcorp.com&extract=true", + json={ + "tenant_id": "cc74fc12-4142-400e-a653-f98bdeadbeef", + "tenant_name": "evilcorp", + "email_domains": ["evilcorp.com"], + }, ) + module_test.httpx_mock.add_response( + url="https://odc.officeapps.live.com/odc/v2.1/federationprovider?domain=evilcorp.com", + json={}, + ) + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/evilcorp.com/.well-known/openid-configuration", + json={}, + ) + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/common/GetCredentialType", + method="POST", + json={ + "Credentials": { + "FederationRedirectUrl": "https://evilcorp.okta.com/app/office365/deadbeef/sso/wsfed/passive", + }, + "EstsProperties": {}, + }, + ) + module_test.httpx_mock.add_response( + url="https://login.microsoftonline.com/common/userrealm/test@evilcorp.com?api-version=2.0", + json={}, + ) + module_test.httpx_mock.add_response( + url="https://mta-sts.evilcorp.com/.well-known/mta-sts.txt", + status_code=404, + ) + + # oauth module mocks module_test.httpx_mock.add_response( url="https://login.windows.net/evilcorp.com/.well-known/openid-configuration", json=self.openid_config_azure, @@ -228,4 +260,4 @@ def check(self, module_test, events): == "Potentially Sprayable OAUTH Endpoint (domain: evilcorp.com) at https://evilcorp.okta.com/oauth2/v1/token" for e in events ) - assert any(e.data == "https://sts.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/" for e in events) + assert any(e.url == "https://sts.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/" for e in events) diff --git a/bbot/test/test_step_2/module_tests/test_module_portfilter.py b/bbot/test/test_step_2/module_tests/test_module_portfilter.py index 605761debb..0003d0189e 100644 --- a/bbot/test/test_step_2/module_tests/test_module_portfilter.py +++ b/bbot/test/test_step_2/module_tests/test_module_portfilter.py @@ -4,7 +4,7 @@ class TestPortfilter_disabled(ModuleTestBase): modules_overrides = [] - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): from bbot.modules.base import BaseModule class DummyModule(BaseModule): @@ -17,14 +17,14 @@ async def handle_event(self, event): "www.blacklanternsecurity.com:443", "OPEN_TCP_PORT", parent=event, - tags=["cdn-ip", "cdn-amazon"], + tags=["cdn", "amazon"], ) # when portfilter is enabled, this should be filtered out await self.emit_event( "www.blacklanternsecurity.com:8080", "OPEN_TCP_PORT", parent=event, - tags=["cdn-ip", "cdn-amazon"], + tags=["cdn", "amazon"], ) await self.emit_event("www.blacklanternsecurity.com:21", "OPEN_TCP_PORT", parent=event) diff --git a/bbot/test/test_step_2/module_tests/test_module_portscan.py b/bbot/test/test_step_2/module_tests/test_module_portscan.py index 2f904e90eb..0efd95b40b 100644 --- a/bbot/test/test_step_2/module_tests/test_module_portscan.py +++ b/bbot/test/test_step_2/module_tests/test_module_portscan.py @@ -74,18 +74,20 @@ async def run_masscan(command, *args, **kwargs): module_test.monkeypatch.setattr(module_test.scan.helpers, "run_live", run_masscan) def check(self, module_test, events): - assert set(self.syn_scanned) == {"8.8.8.0/24", "8.8.4.0/24"} + # The /24 ranges must always be syn-scanned; individual /32s may also appear + # if async event delivery splits targets across multiple batches (observed on Python 3.13+) + assert {"8.8.8.0/24", "8.8.4.0/24"}.issubset(set(self.syn_scanned)) assert set(self.ping_scanned) == set() assert self.syn_runs >= 1 assert self.ping_runs == 0 assert 1 == len( - [e for e in events if e.type == "DNS_NAME" and e.data == "evilcorp.com" and str(e.module) == "TARGET"] + [e for e in events if e.type == "DNS_NAME" and e.data == "evilcorp.com" and str(e.module) == "SEED"] ) assert 1 == len( - [e for e in events if e.type == "DNS_NAME" and e.data == "www.evilcorp.com" and str(e.module) == "TARGET"] + [e for e in events if e.type == "DNS_NAME" and e.data == "www.evilcorp.com" and str(e.module) == "SEED"] ) assert 1 == len( - [e for e in events if e.type == "DNS_NAME" and e.data == "asdf.evilcorp.net" and str(e.module) == "TARGET"] + [e for e in events if e.type == "DNS_NAME" and e.data == "asdf.evilcorp.net" and str(e.module) == "SEED"] ) assert 1 == len( [ @@ -131,7 +133,9 @@ class TestPortscanPingFirst(TestPortscan): def check(self, module_test, events): assert set(self.syn_scanned) == {"8.8.8.8/32"} - assert set(self.ping_scanned) == {"8.8.8.0/24", "8.8.4.0/24"} + # The /24 ranges must always be ping-scanned; individual /32s may also appear + # if async event delivery splits targets across multiple batches (observed on Python 3.14+) + assert {"8.8.8.0/24", "8.8.4.0/24"}.issubset(set(self.ping_scanned)) assert self.syn_runs == 1 assert self.ping_runs >= 1 open_port_events = [e for e in events if e.type == "OPEN_TCP_PORT"] diff --git a/bbot/test/test_step_2/module_tests/test_module_postgres.py b/bbot/test/test_step_2/module_tests/test_module_postgres.py index ea6c00210c..8c52eabebe 100644 --- a/bbot/test/test_step_2/module_tests/test_module_postgres.py +++ b/bbot/test/test_step_2/module_tests/test_module_postgres.py @@ -1,4 +1,3 @@ -import time import asyncio from .base import ModuleTestBase @@ -25,27 +24,8 @@ async def setup_before_prep(self, module_test): "postgres", ) - import asyncpg - # wait for the container to start - start_time = time.time() - while True: - try: - # Connect to the default 'postgres' database to create 'bbot' - conn = await asyncpg.connect( - user="postgres", password="bbotislife", database="postgres", host="127.0.0.1" - ) - await conn.execute("CREATE DATABASE bbot") - await conn.close() - break - except asyncpg.exceptions.DuplicateDatabaseError: - # If the database already exists, break the loop - break - except Exception as e: - if time.time() - start_time > 60: # timeout after 60 seconds - self.log.error("PostgreSQL server did not start in time.") - raise e - await asyncio.sleep(1) + await self.wait_for_port_open(5432) if process.returncode != 0: self.log.error("Failed to start PostgreSQL server") diff --git a/bbot/test/test_step_2/module_tests/test_module_rabbitmq.py b/bbot/test/test_step_2/module_tests/test_module_rabbitmq.py new file mode 100644 index 0000000000..1ec6c1eb56 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_rabbitmq.py @@ -0,0 +1,72 @@ +import json +import asyncio +from contextlib import suppress + +from .base import ModuleTestBase + + +class TestRabbitMQ(ModuleTestBase): + config_overrides = { + "modules": { + "rabbitmq": { + "url": "amqp://guest:guest@localhost/", + "queue": "bbot_events", + } + } + } + skip_distro_tests = True + + async def setup_before_prep(self, module_test): + import aio_pika + + # Start RabbitMQ + await asyncio.create_subprocess_exec( + "docker", "run", "-d", "--rm", "--name", "bbot-test-rabbitmq", "-p", "5672:5672", "rabbitmq:3-management" + ) + + # Wait for RabbitMQ to be ready + while True: + try: + # Attempt to connect to RabbitMQ with a timeout + connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/") + break # Exit the loop if the connection is successful + except Exception as e: + with suppress(Exception): + await connection.close() + self.log.verbose(f"Waiting for RabbitMQ to be ready: {e}") + await asyncio.sleep(0.5) # Wait a bit before retrying + + async def check(self, module_test, events): + import aio_pika + + connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/") + channel = await connection.channel() + queue = await channel.declare_queue("bbot_events", durable=True) + + try: + events_json = [e.json() for e in events] + events_json.sort(key=lambda x: x["timestamp"]) + + # Collect events from RabbitMQ + rabbitmq_events = [] + async with queue.iterator() as queue_iter: + async for message in queue_iter: + async with message.process(): + event_data = json.loads(message.body.decode("utf-8")) + rabbitmq_events.append(event_data) + if len(rabbitmq_events) >= len(events_json): + break + + rabbitmq_events.sort(key=lambda x: x["timestamp"]) + + # Verify the events match + assert events_json == rabbitmq_events, "Events do not match" + + finally: + # Clean up: Close the RabbitMQ connection + await connection.close() + # Stop RabbitMQ container + process = await asyncio.create_subprocess_exec( + "docker", "stop", "bbot-test-rabbitmq", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + await process.communicate() diff --git a/bbot/test/test_step_2/module_tests/test_module_retirejs.py b/bbot/test/test_step_2/module_tests/test_module_retirejs.py index 0cf09a8eae..3afbb6e593 100644 --- a/bbot/test/test_step_2/module_tests/test_module_retirejs.py +++ b/bbot/test/test_step_2/module_tests/test_module_retirejs.py @@ -97,7 +97,7 @@ def check(self, module_test, events): # The third, non-vulnerable URL (lodash.min.js) is not output because it's a "special URL", and # nothing interesting has been discovered from it. vuln_urls = {"http://127.0.0.1:8888/handlebars.min.js", "http://127.0.0.1:8888/jquery-3.4.1.min.js"} - assert {e.data for e in js_url_events} == vuln_urls, "Expected to find the vulnerable URLs in the output" + assert {e.url for e in js_url_events} == vuln_urls, "Expected to find the vulnerable URLs in the output" # Check for FINDING events generated by retirejs finding_events = [e for e in events if e.type == "FINDING"] @@ -132,6 +132,8 @@ def check(self, module_test, events): for finding in retirejs_findings: assert "description" in finding.data, "Finding should have description" assert "url" in finding.data, "Finding should have url" + # url field should point to the page that loaded the JS, not the JS file itself + assert finding.data["url"] == "http://127.0.0.1:8888/", "url should be the parent page URL" assert finding.parent.type == "URL_UNVERIFIED", "Parent should be URL_UNVERIFIED" diff --git a/bbot/test/test_step_2/module_tests/test_module_robots.py b/bbot/test/test_step_2/module_tests/test_module_robots.py index 3d9156bb4c..a6898f4128 100644 --- a/bbot/test/test_step_2/module_tests/test_module_robots.py +++ b/bbot/test/test_step_2/module_tests/test_module_robots.py @@ -22,18 +22,18 @@ def check(self, module_test, events): for e in events: if e.type == "URL_UNVERIFIED": - if str(e.module) != "TARGET": + if str(e.module) != "SEED": assert "spider-danger" in e.tags, f"{e} doesn't have spider-danger tag" - if e.data == "http://127.0.0.1:8888/allow/": + if e.url == "http://127.0.0.1:8888/allow/": allow_bool = True - if e.data == "http://127.0.0.1:8888/disallow/": + if e.url == "http://127.0.0.1:8888/disallow/": disallow_bool = True - if e.data == "http://127.0.0.1:8888/sitemap.txt": + if e.url == "http://127.0.0.1:8888/sitemap.txt": sitemap_bool = True - if re.match(r"http://127\.0\.0\.1:8888/\w+/wildcard\.txt", e.data): + if re.match(r"http://127\.0\.0\.1:8888/\w+/wildcard\.txt", e.url): wildcard_bool = True assert allow_bool diff --git a/bbot/test/test_step_2/module_tests/test_module_securitytxt.py b/bbot/test/test_step_2/module_tests/test_module_securitytxt.py index 0fa897c222..f6bf31c883 100644 --- a/bbot/test/test_step_2/module_tests/test_module_securitytxt.py +++ b/bbot/test/test_step_2/module_tests/test_module_securitytxt.py @@ -25,7 +25,7 @@ def check(self, module_test, events): "Failed to detect email address" ) assert not any( - e.type == "URL_UNVERIFIED" and e.data == "https://blacklanternsecurity.notreal/.well-known/security.txt" + e.type == "URL_UNVERIFIED" and e.url == "https://blacklanternsecurity.notreal/.well-known/security.txt" for e in events ), "Failed to filter Canonical URL to self" assert not any(str(e.data) == "vdp@example.com" for e in events) @@ -39,12 +39,12 @@ class TestSecurityTxtEmailsFalse(TestSecurityTxt): def check(self, module_test, events): assert not any(e.type == "EMAIL_ADDRESS" for e in events), "Detected email address when emails=False" - assert any(e.type == "URL_UNVERIFIED" and e.data == "https://vdp.example.com/" for e in events), ( + assert any(e.type == "URL_UNVERIFIED" and e.url == "https://vdp.example.com/" for e in events), ( "Failed to detect URL" ) - assert any(e.type == "URL_UNVERIFIED" and e.data == "https://example.com/cert" for e in events), ( + assert any(e.type == "URL_UNVERIFIED" and e.url == "https://example.com/cert" for e in events), ( "Failed to detect URL" ) - assert any(e.type == "URL_UNVERIFIED" and e.data == "https://www.careers.example.com/" for e in events), ( + assert any(e.type == "URL_UNVERIFIED" and e.url == "https://www.careers.example.com/" for e in events), ( "Failed to detect URL" ) diff --git a/bbot/test/test_step_2/module_tests/test_module_shodan_dns.py b/bbot/test/test_step_2/module_tests/test_module_shodan_dns.py index 3731220488..d2aaa99c8e 100644 --- a/bbot/test/test_step_2/module_tests/test_module_shodan_dns.py +++ b/bbot/test/test_step_2/module_tests/test_module_shodan_dns.py @@ -24,6 +24,8 @@ async def setup_before_prep(self, module_test): ], }, ) + + async def setup_after_prep(self, module_test): await module_test.mock_dns( { "blacklanternsecurity.com": { diff --git a/bbot/test/test_step_2/module_tests/test_module_shodan_enterprise.py b/bbot/test/test_step_2/module_tests/test_module_shodan_enterprise.py new file mode 100644 index 0000000000..2b18f20520 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_shodan_enterprise.py @@ -0,0 +1,136 @@ +from .base import ModuleTestBase + + +class TestShodan_Enterprise(ModuleTestBase): + targets = ["8.8.8.8"] + config_overrides = {"modules": {"shodan_enterprise": {"api_key": "deadbeef"}}} + + async def setup_before_prep(self, module_test): + module_test.httpx_mock.add_response( + url="https://api.shodan.io/shodan/host/8.8.8.8?key=deadbeef", + json={ + "asn": "AS15169", + "org": "Google LLC", + "isp": "Google LLC", + "country_code": "US", + "tags": ["cloud", "public-dns", "verified"], + "data": [ + { + "ip_str": "8.8.8.8", + "port": 53, + "transport": "tcp", + "product": "Google Public DNS", + "tags": ["dns", "nameserver"], + "cpe": ["cpe:/a:google:dns"], + "cpe23": ["cpe:2.3:a:google:dns:1.0:*:*:*:*:*:*:*"], + "http": { + "components": { + "OpenSSL": {"categories": ["web-crypto"]}, + "nginx": {"categories": ["web-servers"]}, + } + }, + "vulns": { + "CVE-2021-12345": {"cvss": 7.5}, + "CVE-2022-11111": {"cvss": 9.7}, + "CVE-2020-00001": {"cvss": 2.5}, + }, + }, + { + "ip_str": "8.8.8.8", + "port": 53, + "transport": "udp", + "product": "Google Public DNS", + "tags": ["dns"], + "cpe": [], + "cpe23": [], + "http": {}, + "vulns": {}, + }, + ], + }, + ) + + def check(self, module_test, events): + tcp_ports = [e.data for e in events if e.type == "OPEN_TCP_PORT"] + udp_ports = [e.data for e in events if e.type == "OPEN_UDP_PORT"] + assert any("8.8.8.8:53" in str(p) for p in tcp_ports), "TCP port 53 not detected" + assert any("8.8.8.8:53" in str(p) for p in udp_ports), "UDP port 53 not detected" + finding_events = [e for e in events if e.type == "FINDING"] + finding_map = {e.data.get("description"): e.data.get("severity") for e in finding_events} + assert "CVE-2021-12345" in finding_map + assert finding_map["CVE-2021-12345"] == "HIGH" + assert "CVE-2020-00001" in finding_map + assert finding_map["CVE-2020-00001"] == "LOW" + tech_events = [e for e in events if e.type == "TECHNOLOGY"] + tech_names = {e.data.get("technology") for e in tech_events} + assert "cpe:/a:google:dns" in tech_names + assert "google public dns" in tech_names + assert "openssl" in tech_names + assert "nginx" in tech_names + + +shodan_response_1_1_1_1 = { + "data": [ + { + "ip_str": "1.1.1.1", + "port": 80, + "transport": "tcp", + "product": "cloudflare", + "tags": [], + "cpe": [], + "cpe23": [], + "http": {}, + "vulns": {}, + }, + ], +} + + +class TestShodan_Enterprise_InScopeOnly(ModuleTestBase): + """Test that in_scope_only=True (default) does NOT query out-of-scope IPs.""" + + targets = ["evilcorp.notreal"] + module_name = "shodan_enterprise" + config_overrides = {"modules": {"shodan_enterprise": {"api_key": "deadbeef"}}} + + async def setup_before_prep(self, module_test): + # This should NOT be called because in_scope_only=True + module_test.httpx_mock.add_response( + url="https://api.shodan.io/shodan/host/1.1.1.1?key=deadbeef", + json=shodan_response_1_1_1_1, + ) + + async def setup_after_prep(self, module_test): + await module_test.mock_dns({"evilcorp.notreal": {"A": ["1.1.1.1"]}}) + + def check(self, module_test, events): + assert not any(e.type == "OPEN_TCP_PORT" and "1.1.1.1" in e.data for e in events), ( + "Should not have queried out-of-scope IP with in_scope_only=True" + ) + + +class TestShodan_Enterprise_OutOfScope(ModuleTestBase): + """Test that in_scope_only=False DOES query out-of-scope IPs (up to distance 1).""" + + targets = ["evilcorp.notreal"] + module_name = "shodan_enterprise" + config_overrides = { + "modules": {"shodan_enterprise": {"api_key": "deadbeef", "in_scope_only": False}}, + "dns": {"minimal": False}, + "scope": {"report_distance": 1}, + } + + async def setup_before_prep(self, module_test): + # This SHOULD be called because in_scope_only=False + module_test.httpx_mock.add_response( + url="https://api.shodan.io/shodan/host/1.1.1.1?key=deadbeef", + json=shodan_response_1_1_1_1, + ) + + async def setup_after_prep(self, module_test): + await module_test.mock_dns({"evilcorp.notreal": {"A": ["1.1.1.1"]}}) + + def check(self, module_test, events): + assert any(e.type == "OPEN_TCP_PORT" and e.data == "1.1.1.1:80" for e in events), ( + "Should have queried out-of-scope IP with in_scope_only=False" + ) diff --git a/bbot/test/test_step_2/module_tests/test_module_shodan_idb.py b/bbot/test/test_step_2/module_tests/test_module_shodan_idb.py index 482a355856..b4cd0a6344 100644 --- a/bbot/test/test_step_2/module_tests/test_module_shodan_idb.py +++ b/bbot/test/test_step_2/module_tests/test_module_shodan_idb.py @@ -4,7 +4,7 @@ class TestShodan_IDB(ModuleTestBase): config_overrides = {"dns": {"minimal": False}} - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): await module_test.mock_dns( { "blacklanternsecurity.com": {"A": ["1.2.3.4"]}, diff --git a/bbot/test/test_step_2/module_tests/test_module_slack.py b/bbot/test/test_step_2/module_tests/test_module_slack.py index 1258ed5110..9063552722 100644 --- a/bbot/test/test_step_2/module_tests/test_module_slack.py +++ b/bbot/test/test_step_2/module_tests/test_module_slack.py @@ -4,4 +4,4 @@ class TestSlack(DiscordBase): modules_overrides = ["slack", "excavate", "badsecrets", "httpx"] webhook_url = "https://hooks.slack.com/services/deadbeef/deadbeef/deadbeef" - config_overrides = {"modules": {"slack": {"webhook_url": webhook_url}}} + config_overrides = {"modules": {"slack": {"webhook_url": webhook_url, "min_severity": "INFO"}}} diff --git a/bbot/test/test_step_2/module_tests/test_module_speculate.py b/bbot/test/test_step_2/module_tests/test_module_speculate.py index 777568ef8d..38881947bd 100644 --- a/bbot/test/test_step_2/module_tests/test_module_speculate.py +++ b/bbot/test/test_step_2/module_tests/test_module_speculate.py @@ -5,7 +5,7 @@ class TestSpeculate_Subdirectories(ModuleTestBase): targets = ["http://127.0.0.1:8888/subdir1/subdir2/"] modules_overrides = ["httpx", "speculate"] - async def setup_after_prep(self, module_test): + async def setup_before_prep(self, module_test): expect_args = {"method": "GET", "uri": "/"} respond_args = {"response_data": "alive"} module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) @@ -19,7 +19,7 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - assert any(e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/subdir1/" for e in events) + assert any(e.type == "URL_UNVERIFIED" and e.url == "http://127.0.0.1:8888/subdir1/" for e in events) class TestSpeculate_OpenPorts(ModuleTestBase): @@ -27,7 +27,7 @@ class TestSpeculate_OpenPorts(ModuleTestBase): modules_overrides = ["speculate", "certspotter", "shodan_idb"] config_overrides = {"speculate": True} - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): await module_test.mock_dns( { "evilcorp.com": {"A": ["127.0.254.1"]}, @@ -35,11 +35,6 @@ async def setup_before_prep(self, module_test): } ) - module_test.httpx_mock.add_response( - url="https://api.certspotter.com/v1/issuances?domain=evilcorp.com&include_subdomains=true&expand=dns_names", - json=[{"dns_names": ["*.asdf.evilcorp.com"]}], - ) - from bbot.modules.base import BaseModule class DummyModule(BaseModule): @@ -55,7 +50,21 @@ async def setup(self): async def handle_event(self, event): self.events.append(event) - module_test.scan.modules["dummy"] = DummyModule(module_test.scan) + dummy_module = DummyModule(module_test.scan) + await dummy_module.setup() + module_test.scan.modules["dummy"] = dummy_module + + # Manually configure speculate module to emit OPEN_TCP_PORT events + # since the dummy module was added after speculate's setup phase + speculate_module = module_test.scan.modules["speculate"] + speculate_module.open_port_consumers = True + speculate_module.emit_open_ports = True + + async def setup_before_prep(self, module_test): + module_test.httpx_mock.add_response( + url="https://api.certspotter.com/v1/issuances?domain=evilcorp.com&include_subdomains=true&expand=dns_names", + json=[{"dns_names": ["*.asdf.evilcorp.com"]}], + ) def check(self, module_test, events): events_data = set() @@ -72,6 +81,36 @@ class TestSpeculate_OpenPorts_Portscanner(TestSpeculate_OpenPorts): modules_overrides = ["speculate", "certspotter", "portscan"] config_overrides = {"speculate": True} + async def setup_after_prep(self, module_test): + await module_test.mock_dns( + { + "evilcorp.com": {"A": ["127.0.254.1"]}, + "asdf.evilcorp.com": {"A": ["127.0.254.2"]}, + } + ) + + from bbot.modules.base import BaseModule + + class DummyModule(BaseModule): + _name = "dummy" + watched_events = ["OPEN_TCP_PORT"] + scope_distance_modifier = 10 + accept_dupes = True + + async def setup(self): + self.events = [] + return True + + async def handle_event(self, event): + self.events.append(event) + + dummy_module = DummyModule(module_test.scan) + await dummy_module.setup() + module_test.scan.modules["dummy"] = dummy_module + + # DON'T manually configure speculate module here - we want it to detect + # the portscan module and NOT emit OPEN_TCP_PORT events + def check(self, module_test, events): events_data = set() for e in module_test.scan.modules["dummy"].events: diff --git a/bbot/test/test_step_2/module_tests/test_module_splunk.py b/bbot/test/test_step_2/module_tests/test_module_splunk.py index 8366a6289b..a849055d2b 100644 --- a/bbot/test/test_step_2/module_tests/test_module_splunk.py +++ b/bbot/test/test_step_2/module_tests/test_module_splunk.py @@ -23,7 +23,7 @@ def verify_data(self, j): if not j["index"] == "bbot_index": return False data = j["event"] - if not data["data"] == "blacklanternsecurity.com" and data["type"] == "DNS_NAME": + if not data["data_json"] == "blacklanternsecurity.com" and data["type"] == "DNS_NAME": return False return True diff --git a/bbot/test/test_step_2/module_tests/test_module_sqlite.py b/bbot/test/test_step_2/module_tests/test_module_sqlite.py index ec80b7555d..7970627b15 100644 --- a/bbot/test/test_step_2/module_tests/test_module_sqlite.py +++ b/bbot/test/test_step_2/module_tests/test_module_sqlite.py @@ -8,6 +8,8 @@ class TestSQLite(ModuleTestBase): def check(self, module_test, events): sqlite_output_file = module_test.scan.home / "output.sqlite" assert sqlite_output_file.exists(), "SQLite output file not found" + + # first connect with raw sqlite with sqlite3.connect(sqlite_output_file) as db: cursor = db.cursor() results = cursor.execute("SELECT * FROM event").fetchall() @@ -16,3 +18,15 @@ def check(self, module_test, events): assert len(results) == 1, "No scans found in SQLite database" results = cursor.execute("SELECT * FROM target").fetchall() assert len(results) == 1, "No targets found in SQLite database" + + # then connect with bbot models + from bbot.models.sql import Event + from sqlmodel import create_engine, Session, select + + engine = create_engine(f"sqlite:///{sqlite_output_file}") + + with Session(engine) as session: + statement = select(Event).where(Event.host == "evilcorp.com") + event = session.exec(statement).first() + assert event.host == "evilcorp.com", "Event host should match target host" + assert event.data == "evilcorp.com", "Event data should match target host" diff --git a/bbot/test/test_step_2/module_tests/test_module_stdout.py b/bbot/test/test_step_2/module_tests/test_module_stdout.py index 27d8a30594..a77a2a3f89 100644 --- a/bbot/test/test_step_2/module_tests/test_module_stdout.py +++ b/bbot/test/test_step_2/module_tests/test_module_stdout.py @@ -9,7 +9,7 @@ class TestStdout(ModuleTestBase): def check(self, module_test, events): out, err = module_test.capsys.readouterr() assert out.startswith("[SCAN] \tteststdout") - assert "[DNS_NAME] \tblacklanternsecurity.com\tTARGET" in out + assert "[DNS_NAME] \tblacklanternsecurity.com\tSEED" in out class TestStdoutEventTypes(TestStdout): @@ -18,7 +18,7 @@ class TestStdoutEventTypes(TestStdout): def check(self, module_test, events): out, err = module_test.capsys.readouterr() assert len(out.splitlines()) == 1 - assert out.startswith("[DNS_NAME] \tblacklanternsecurity.com\tTARGET") + assert out.startswith("[DNS_NAME] \tblacklanternsecurity.com\tSEED") class TestStdoutEventFields(TestStdout): diff --git a/bbot/test/test_step_2/module_tests/test_module_subdomainradar.py b/bbot/test/test_step_2/module_tests/test_module_subdomainradar.py index c2bb827f35..9e53c8f667 100644 --- a/bbot/test/test_step_2/module_tests/test_module_subdomainradar.py +++ b/bbot/test/test_step_2/module_tests/test_module_subdomainradar.py @@ -4,7 +4,7 @@ class TestSubDomainRadar(ModuleTestBase): config_overrides = {"modules": {"subdomainradar": {"api_key": "asdf"}}} - async def setup_before_prep(self, module_test): + async def setup_after_prep(self, module_test): await module_test.mock_dns( { "blacklanternsecurity.com": {"A": ["127.0.0.88"]}, @@ -12,6 +12,8 @@ async def setup_before_prep(self, module_test): "asdf.blacklanternsecurity.com": {"A": ["127.0.0.88"]}, } ) + + async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( url="https://api.subdomainradar.io/profile", match_headers={"Authorization": "Bearer asdf"}, diff --git a/bbot/test/test_step_2/module_tests/test_module_teams.py b/bbot/test/test_step_2/module_tests/test_module_teams.py index 3f573dc21b..846adb7ec3 100644 --- a/bbot/test/test_step_2/module_tests/test_module_teams.py +++ b/bbot/test/test_step_2/module_tests/test_module_teams.py @@ -7,7 +7,7 @@ class TestTeams(DiscordBase): modules_overrides = ["teams", "excavate", "badsecrets", "httpx"] webhook_url = "https://evilcorp.webhook.office.com/webhookb2/deadbeef@deadbeef/IncomingWebhook/deadbeef/deadbeef" - config_overrides = {"modules": {"teams": {"webhook_url": webhook_url, "retries": 5}}} + config_overrides = {"modules": {"teams": {"webhook_url": webhook_url, "retries": 5, "min_severity": "INFO"}}} async def setup_after_prep(self, module_test): self.custom_setup(module_test) @@ -32,8 +32,6 @@ def custom_response(request: httpx.Request): module_test.httpx_mock.add_callback(custom_response, url=self.webhook_url) def check(self, module_test, events): - vulns = [e for e in events if e.type == "VULNERABILITY"] findings = [e for e in events if e.type == "FINDING"] - assert len(findings) == 1 - assert len(vulns) == 2 + assert len(findings) == 3 assert module_test.request_count == 5 diff --git a/bbot/test/test_step_2/module_tests/test_module_telerik.py b/bbot/test/test_step_2/module_tests/test_module_telerik.py index c401100bbe..71fa6bf1c4 100644 --- a/bbot/test/test_step_2/module_tests/test_module_telerik.py +++ b/bbot/test/test_step_2/module_tests/test_module_telerik.py @@ -91,7 +91,7 @@ def check(self, module_test, events): telerik_axd_detection = True continue - if e.type == "VULNERABILITY" and "Confirmed Vulnerable Telerik (version: 2014.3.1024)": + if e.type == "FINDING" and "Confirmed Vulnerable Telerik (version: 2014.3.1024)" in e.data["description"]: telerik_axd_vulnerable = True continue diff --git a/bbot/test/test_step_2/module_tests/test_module_trajan.py b/bbot/test/test_step_2/module_tests/test_module_trajan.py new file mode 100644 index 0000000000..978bb23d7d --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_trajan.py @@ -0,0 +1,335 @@ +import json +from subprocess import CompletedProcess + +from .base import ModuleTestBase + + +trajan_github_output = json.dumps( + { + "summary": {"repositories": 1, "workflows": 3, "findings": 1, "errors": 0}, + "findings": [ + { + "type": "pwn_request", + "severity": "critical", + "confidence": "high", + "complexity": "zero_click", + "platform": "github", + "class": "injection", + "repository": "blacklanternsecurity/bbot", + "workflow": "CI", + "workflow_file": ".github/workflows/ci.yml", + "job": "build", + "step": "checkout", + "line": 42, + "trigger": "pull_request_target", + "evidence": "on: pull_request_target\nwith:\n ref: ${{ github.event.pull_request.head.sha }}", + "remediation": "Avoid using pull_request_target with user-controlled refs", + } + ], + } +) + +trajan_gitlab_output = json.dumps( + { + "summary": {"repositories": 1, "workflows": 2, "findings": 1, "errors": 0}, + "findings": [ + { + "type": "token_exposure", + "severity": "high", + "confidence": "high", + "platform": "gitlab", + "class": "secrets_exposure", + "repository": "someorg/somerepo", + "workflow": "deploy", + "workflow_file": ".gitlab-ci.yml", + "evidence": "CI_JOB_TOKEN exposed in script block via echo $CI_JOB_TOKEN", + } + ], + } +) + +trajan_ado_output = json.dumps( + { + "summary": {"repositories": 1, "workflows": 1, "findings": 1, "errors": 0}, + "findings": [ + { + "type": "script_injection", + "severity": "high", + "confidence": "medium", + "platform": "azuredevops", + "class": "injection", + "repository": "myproject/myrepo", + "workflow": "Build Pipeline", + "workflow_file": "azure-pipelines.yml", + "job": "build", + "step": "run_script", + "line": 15, + "trigger": "pullRequest", + "evidence": "script: echo $(Build.SourceBranch)", + } + ], + } +) + +trajan_jfrog_output = json.dumps( + { + "instance": "https://mycompany.jfrog.io", + "artifactSecrets": [ + { + "artifact": "config.yaml", + "path": "libs-release/com/example/config.yaml", + "repo": "libs-release", + "secretTypes": ["aws_access_key", "generic_api_key"], + "value": "AKIA...", + } + ], + "buildSecrets": [ + { + "buildName": "release-pipeline", + "buildNumber": "42", + "envVar": "AWS_SECRET_KEY", + "value": "wJalr...", + "secretTypes": ["aws_secret_key"], + } + ], + "remoteRepoCredentials": [], + "totalSecrets": 2, + } +) + +trajan_jenkins_output = json.dumps( + { + "summary": {"repositories": 1, "workflows": 4, "findings": 1, "errors": 0}, + "findings": [ + { + "type": "jenkins_script_console", + "severity": "critical", + "confidence": "high", + "platform": "jenkins", + "class": "configuration", + "repository": "deploy-pipeline", + "workflow": "deploy-pipeline", + "evidence": "Script console accessible at /script without authentication", + } + ], + } +) + + +class TestTrajanGithub(ModuleTestBase): + targets = ["https://github.com/blacklanternsecurity/bbot"] + modules_overrides = ["trajan"] + config_overrides = {"modules": {"trajan": {"github_token": "test-gh-token"}}} + + async def setup_after_prep(self, module_test): + async def mock_run(*command, **kwargs): + cmd = command[0] if len(command) == 1 and isinstance(command[0], list) else list(command) + if "trajan" in cmd: + assert "github" in cmd + assert "--repo" in cmd + repo_idx = cmd.index("--repo") + assert cmd[repo_idx + 1] == "blacklanternsecurity/bbot" + assert "--token" in cmd + token_idx = cmd.index("--token") + assert cmd[token_idx + 1] == "test-gh-token" + return CompletedProcess(cmd, 0, trajan_github_output, "") + return CompletedProcess(cmd, 1, "", "") + + module_test.monkeypatch.setattr(module_test.scan.helpers, "run", mock_run) + + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + assert len(findings) == 1, f"Expected 1 FINDING, got {len(findings)}" + finding = findings[0] + assert "pwn_request" in finding.data["name"] + assert finding.data["severity"] == "CRITICAL" + assert "CI" in finding.data["description"] + + +class TestTrajanGitlab(ModuleTestBase): + targets = ["https://gitlab.com/someorg/somerepo"] + modules_overrides = ["trajan"] + config_overrides = {"modules": {"trajan": {"gitlab_token": "test-gl-token"}}} + + async def setup_after_prep(self, module_test): + async def mock_run(*command, **kwargs): + cmd = command[0] if len(command) == 1 and isinstance(command[0], list) else list(command) + if "trajan" in cmd: + assert "gitlab" in cmd + assert "--project" in cmd + project_idx = cmd.index("--project") + assert cmd[project_idx + 1] == "someorg/somerepo" + assert "--token" in cmd + token_idx = cmd.index("--token") + assert cmd[token_idx + 1] == "test-gl-token" + return CompletedProcess(cmd, 0, trajan_gitlab_output, "") + return CompletedProcess(cmd, 1, "", "") + + module_test.monkeypatch.setattr(module_test.scan.helpers, "run", mock_run) + + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + assert len(findings) == 1, f"Expected 1 FINDING, got {len(findings)}" + finding = findings[0] + assert "token_exposure" in finding.data["name"] + assert finding.data["severity"] == "HIGH" + assert "deploy" in finding.data["description"] + + +class TestTrajanAdo(ModuleTestBase): + targets = ["https://dev.azure.com/myorg/myproject/_git/myrepo"] + modules_overrides = ["trajan"] + config_overrides = {"modules": {"trajan": {"ado_token": "test-ado-token"}}} + + async def setup_after_prep(self, module_test): + async def mock_run(*command, **kwargs): + cmd = command[0] if len(command) == 1 and isinstance(command[0], list) else list(command) + if "trajan" in cmd: + assert "ado" in cmd + assert "--org" in cmd + org_idx = cmd.index("--org") + assert cmd[org_idx + 1] == "myorg" + assert "--repo" in cmd + repo_idx = cmd.index("--repo") + assert cmd[repo_idx + 1] == "myproject/myrepo" + assert "--token" in cmd + token_idx = cmd.index("--token") + assert cmd[token_idx + 1] == "test-ado-token" + return CompletedProcess(cmd, 0, trajan_ado_output, "") + return CompletedProcess(cmd, 1, "", "") + + module_test.monkeypatch.setattr(module_test.scan.helpers, "run", mock_run) + + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + assert len(findings) == 1, f"Expected 1 FINDING, got {len(findings)}" + finding = findings[0] + assert "script_injection" in finding.data["name"] + assert finding.data["severity"] == "HIGH" + assert "Build Pipeline" in finding.data["description"] + + +class TestTrajanJfrog(ModuleTestBase): + targets = ["https://mycompany.jfrog.io/artifactory/libs-release"] + modules_overrides = ["trajan"] + config_overrides = {"modules": {"trajan": {"jfrog_token": "test-jfrog-token"}}} + + async def setup_after_prep(self, module_test): + async def mock_run(*command, **kwargs): + cmd = command[0] if len(command) == 1 and isinstance(command[0], list) else list(command) + if "trajan" in cmd: + assert "jfrog" in cmd + assert "--url" in cmd + url_idx = cmd.index("--url") + assert cmd[url_idx + 1] == "https://mycompany.jfrog.io" + assert "--secrets" in cmd + assert "--token" in cmd + token_idx = cmd.index("--token") + assert cmd[token_idx + 1] == "test-jfrog-token" + return CompletedProcess(cmd, 0, trajan_jfrog_output, "") + return CompletedProcess(cmd, 1, "", "") + + module_test.monkeypatch.setattr(module_test.scan.helpers, "run", mock_run) + + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + assert len(findings) == 2, f"Expected 2 FINDINGs (1 artifact + 1 build secret), got {len(findings)}" + artifact_findings = [e for e in findings if "Artifact Secret" in e.data["name"]] + assert len(artifact_findings) == 1 + assert "aws_access_key" in artifact_findings[0].data["name"] + assert "config.yaml" in artifact_findings[0].data["description"] + build_findings = [e for e in findings if "Build Secret" in e.data["name"]] + assert len(build_findings) == 1 + assert "aws_secret_key" in build_findings[0].data["name"] + assert "release-pipeline" in build_findings[0].data["description"] + + +class TestTrajanJenkins(ModuleTestBase): + """Test Jenkins detection via TECHNOLOGY event on a non-jenkins hostname.""" + + targets = ["blacklanternsecurity.com"] + modules_overrides = ["trajan"] + config_overrides = {"modules": {"trajan": {"jenkins_token": "test-jenkins-token"}}} + + async def setup_after_prep(self, module_test): + # Inject a TECHNOLOGY event as if gowitness/shodan detected Jenkins + tech_event = module_test.scan.make_event( + { + "technology": "jenkins", + "url": "https://ci.blacklanternsecurity.com:8080/job/deploy-pipeline", + "host": "ci.blacklanternsecurity.com", + }, + "TECHNOLOGY", + parent=module_test.scan.root_event, + ) + await module_test.scan.ingress_module.queue_event(tech_event, {}) + + async def mock_run(*command, **kwargs): + cmd = command[0] if len(command) == 1 and isinstance(command[0], list) else list(command) + if "trajan" in cmd: + assert "jenkins" in cmd + assert "--url" in cmd + url_idx = cmd.index("--url") + assert cmd[url_idx + 1] == "https://ci.blacklanternsecurity.com:8080" + assert "--repo" in cmd + repo_idx = cmd.index("--repo") + assert cmd[repo_idx + 1] == "deploy-pipeline" + assert "--token" in cmd + token_idx = cmd.index("--token") + assert cmd[token_idx + 1] == "test-jenkins-token" + return CompletedProcess(cmd, 0, trajan_jenkins_output, "") + return CompletedProcess(cmd, 1, "", "") + + module_test.monkeypatch.setattr(module_test.scan.helpers, "run", mock_run) + + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + assert len(findings) == 1, f"Expected 1 FINDING, got {len(findings)}" + finding = findings[0] + assert "jenkins_script_console" in finding.data["name"] + assert finding.data["severity"] == "CRITICAL" + + +class TestTrajanJfrogTechnology(ModuleTestBase): + """Test JFrog/Artifactory detection via TECHNOLOGY event on a non-jfrog hostname.""" + + targets = ["blacklanternsecurity.com"] + modules_overrides = ["trajan"] + config_overrides = {"modules": {"trajan": {"jfrog_token": "test-jfrog-token"}}} + + async def setup_after_prep(self, module_test): + # Inject an "artifactory" TECHNOLOGY event on a self-hosted instance + tech_event = module_test.scan.make_event( + { + "technology": "artifactory", + "url": "https://artifacts.blacklanternsecurity.com/", + "host": "artifacts.blacklanternsecurity.com", + }, + "TECHNOLOGY", + parent=module_test.scan.root_event, + ) + await module_test.scan.ingress_module.queue_event(tech_event, {}) + + async def mock_run(*command, **kwargs): + cmd = command[0] if len(command) == 1 and isinstance(command[0], list) else list(command) + if "trajan" in cmd: + assert "jfrog" in cmd + assert "--url" in cmd + url_idx = cmd.index("--url") + assert cmd[url_idx + 1] == "https://artifacts.blacklanternsecurity.com" + assert "--secrets" in cmd + assert "--token" in cmd + token_idx = cmd.index("--token") + assert cmd[token_idx + 1] == "test-jfrog-token" + return CompletedProcess(cmd, 0, trajan_jfrog_output, "") + return CompletedProcess(cmd, 1, "", "") + + module_test.monkeypatch.setattr(module_test.scan.helpers, "run", mock_run) + + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + assert len(findings) == 2, f"Expected 2 FINDINGs (1 artifact + 1 build secret), got {len(findings)}" + artifact_findings = [e for e in findings if "Artifact Secret" in e.data["name"]] + assert len(artifact_findings) == 1 + build_findings = [e for e in findings if "Build Secret" in e.data["name"]] + assert len(build_findings) == 1 diff --git a/bbot/test/test_step_2/module_tests/test_module_trufflehog.py b/bbot/test/test_step_2/module_tests/test_module_trufflehog.py index 49d2d568eb..2bdde295b2 100644 --- a/bbot/test/test_step_2/module_tests/test_module_trufflehog.py +++ b/bbot/test/test_step_2/module_tests/test_module_trufflehog.py @@ -3,7 +3,6 @@ import zipfile import tarfile import subprocess -from copy import copy from pathlib import Path from .base import ModuleTestBase @@ -1147,25 +1146,19 @@ async def setup_after_prep(self, module_test): ) # we need this test to work offline, so we patch git_clone to pull from a local file:// path - old_handle_event = module_test.scan.modules["git_clone"].handle_event + old_clone = module_test.scan.modules["git_clone"].clone_git_repository - async def new_handle_event(event): - if event.type == "CODE_REPOSITORY": - event = copy(event) - data = dict(event.data) - data["url"] = event.data["url"].replace( - "https://github.com/blacklanternsecurity", f"file://{temp_path}" - ) - event.data = data - return await old_handle_event(event) + async def new_clone(repository_url): + repository_url = repository_url.replace("https://github.com/blacklanternsecurity", f"file://{temp_path}") + return await old_clone(repository_url) - module_test.monkeypatch.setattr(module_test.scan.modules["git_clone"], "handle_event", new_handle_event) + module_test.monkeypatch.setattr(module_test.scan.modules["git_clone"], "clone_git_repository", new_clone) def check(self, module_test, events): vuln_events = [ e for e in events - if e.type == "VULNERABILITY" + if e.type == "FINDING" and ( e.data["host"] == "hub.docker.com" or e.data["host"] == "github.com" @@ -1238,7 +1231,7 @@ def check(self, module_test, events): finding_events = [ e for e in events - if e.type == e.type == "FINDING" + if e.type == "FINDING" and ( e.data["host"] == "hub.docker.com" or e.data["host"] == "github.com" @@ -1309,7 +1302,7 @@ def check(self, module_test, events): class TestTrufflehog_RAWText(ModuleTestBase): targets = ["http://127.0.0.1:8888/test.pdf"] - modules_overrides = ["httpx", "trufflehog", "filedownload", "extractous"] + modules_overrides = ["httpx", "trufflehog", "filedownload", "kreuzberg"] download_dir = bbot_test_dir / "test_trufflehog_rawtext" config_overrides = { @@ -1330,3 +1323,6 @@ def check(self, module_test, events): finding_events = [e for e in events if e.type == "FINDING"] assert len(finding_events) == 1 assert "Possible Secret Found" in finding_events[0].data["description"] + # Trufflehog emits HIGH severity and MEDIUM confidence for possible secrets + assert finding_events[0].data["severity"] == "HIGH" + assert finding_events[0].data["confidence"] == "MEDIUM" diff --git a/bbot/test/test_step_2/module_tests/test_module_url_manipulation.py b/bbot/test/test_step_2/module_tests/test_module_url_manipulation.py index 725a96fecf..1961b50ce8 100644 --- a/bbot/test/test_step_2/module_tests/test_module_url_manipulation.py +++ b/bbot/test/test_step_2/module_tests/test_module_url_manipulation.py @@ -34,6 +34,6 @@ def check(self, module_test, events): assert any( e.type == "FINDING" and e.data["description"] - == f"Url Manipulation: [body] Sig: [Modified URL: http://127.0.0.1:8888/?{module_test.module.rand_string}=.xml]" + == f"URL Manipulation: [body] Sig: [Modified URL: http://127.0.0.1:8888/?{module_test.module.rand_string}=.xml]" for e in events ) diff --git a/bbot/test/test_step_2/module_tests/test_module_urlscan.py b/bbot/test/test_step_2/module_tests/test_module_urlscan.py index d108f2f565..de767e358c 100644 --- a/bbot/test/test_step_2/module_tests/test_module_urlscan.py +++ b/bbot/test/test_step_2/module_tests/test_module_urlscan.py @@ -55,4 +55,4 @@ async def setup_after_prep(self, module_test): def check(self, module_test, events): assert any(e.data == "asdf.blacklanternsecurity.com" for e in events), "Failed to detect subdomain" - assert any(e.data == "https://asdf.blacklanternsecurity.com/cna.html" for e in events), "Failed to detect URL" + assert any(e.url == "https://asdf.blacklanternsecurity.com/cna.html" for e in events), "Failed to detect URL" diff --git a/bbot/test/test_step_2/module_tests/test_module_vhost.py b/bbot/test/test_step_2/module_tests/test_module_vhost.py deleted file mode 100644 index 16f9991f6e..0000000000 --- a/bbot/test/test_step_2/module_tests/test_module_vhost.py +++ /dev/null @@ -1,65 +0,0 @@ -from .base import ModuleTestBase, tempwordlist - - -class TestVhost(ModuleTestBase): - targets = ["http://localhost:8888", "secret.localhost"] - modules_overrides = ["httpx", "vhost"] - test_wordlist = ["11111111", "admin", "cloud", "junkword1", "zzzjunkword2"] - config_overrides = { - "modules": { - "vhost": { - "wordlist": tempwordlist(test_wordlist), - } - } - } - - async def setup_after_prep(self, module_test): - expect_args = {"method": "GET", "uri": "/", "headers": {"Host": "admin.localhost:8888"}} - respond_args = {"response_data": "Alive vhost admin"} - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - - expect_args = {"method": "GET", "uri": "/", "headers": {"Host": "cloud.localhost:8888"}} - respond_args = {"response_data": "Alive vhost cloud"} - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - - expect_args = {"method": "GET", "uri": "/", "headers": {"Host": "q-cloud.localhost:8888"}} - respond_args = {"response_data": "Alive vhost q-cloud"} - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - - expect_args = {"method": "GET", "uri": "/", "headers": {"Host": "secret.localhost:8888"}} - respond_args = {"response_data": "Alive vhost secret"} - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - - expect_args = {"method": "GET", "uri": "/", "headers": {"Host": "host.docker.internal"}} - respond_args = {"response_data": "Alive vhost host.docker.internal"} - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - - expect_args = {"method": "GET", "uri": "/"} - respond_args = {"response_data": "alive"} - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - - def check(self, module_test, events): - basic_detection = False - mutaton_of_detected = False - basehost_mutation = False - special_vhost_list = False - wordcloud_detection = False - - for e in events: - if e.type == "VHOST": - if e.data["vhost"] == "admin": - basic_detection = True - if e.data["vhost"] == "cloud": - mutaton_of_detected = True - if e.data["vhost"] == "q-cloud": - basehost_mutation = True - if e.data["vhost"] == "host.docker.internal": - special_vhost_list = True - if e.data["vhost"] == "secret": - wordcloud_detection = True - - assert basic_detection - assert mutaton_of_detected - assert basehost_mutation - assert special_vhost_list - assert wordcloud_detection diff --git a/bbot/test/test_step_2/module_tests/test_module_web_report.py b/bbot/test/test_step_2/module_tests/test_module_web_report.py index 40354e3981..33c2861b0f 100644 --- a/bbot/test/test_step_2/module_tests/test_module_web_report.py +++ b/bbot/test/test_step_2/module_tests/test_module_web_report.py @@ -9,7 +9,7 @@ class TestWebReport(ModuleTestBase): async def setup_before_prep(self, module_test): # trufflehog --> FINDING # dotnetnuke --> TECHNOLOGY - # badsecrets --> VULNERABILITY + # badsecrets --> FINDING respond_args = {"response_data": web_body} module_test.set_expect_requests(respond_args=respond_args) @@ -17,7 +17,9 @@ def check(self, module_test, events): report_file = module_test.scan.home / "web_report.html" with open(report_file) as f: report_content = f.read() - assert "
  • [CRITICAL] Known Secret Found" in report_content + assert "
  • Severity: [CRITICAL] Confidence: [" in report_content + assert "CONFIRMED" in report_content + assert "Known Secret Found" in report_content assert ( """

    URL

      @@ -26,7 +28,7 @@ def check(self, module_test, events): ) assert """Possible Secret Found. Detector Type: [PrivateKey]""" in report_content assert "

      TECHNOLOGY

      " in report_content - assert "
    • DotNetNuke
    • " in report_content + assert "
    • dotnetnuke
    • " in report_content web_body = """ diff --git a/bbot/test/test_step_2/module_tests/test_module_wpscan.py b/bbot/test/test_step_2/module_tests/test_module_wpscan.py index 7e65c1dcce..0e4c1bef3c 100644 --- a/bbot/test/test_step_2/module_tests/test_module_wpscan.py +++ b/bbot/test/test_step_2/module_tests/test_module_wpscan.py @@ -1076,8 +1076,7 @@ async def wpscan_mock_run(*command, **kwargs): def check(self, module_test, events): findings = [e for e in events if e.type == "FINDING"] - vulnerabilities = [e for e in events if e.type == "VULNERABILITY"] technologies = [e for e in events if e.type == "TECHNOLOGY"] - assert len(findings) == 1 - assert len(vulnerabilities) == 59 + # Original expectation: 1 finding + 59 vulnerabilities = 60 FINDING events (all are now FINDING events) + assert len(findings) == 60, f"Expected 60 FINDING events, got {len(findings)}" assert len(technologies) == 4 diff --git a/bbot/test/test_step_2/module_tests/test_module_zeromq.py b/bbot/test/test_step_2/module_tests/test_module_zeromq.py new file mode 100644 index 0000000000..8c118570ef --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_zeromq.py @@ -0,0 +1,46 @@ +import json +import zmq +import zmq.asyncio + +from .base import ModuleTestBase + + +class TestZeroMQ(ModuleTestBase): + config_overrides = { + "modules": { + "zeromq": { + "zmq_address": "tcp://localhost:5555", + } + } + } + + async def setup_before_prep(self, module_test): + # Setup ZeroMQ context and socket + self.context = zmq.asyncio.Context() + self.socket = self.context.socket(zmq.SUB) + self.socket.connect("tcp://localhost:5555") + self.socket.setsockopt_string(zmq.SUBSCRIBE, "") + + async def check(self, module_test, events): + try: + events_json = [e.json() for e in events] + events_json.sort(key=lambda x: x["timestamp"]) + + # Collect events from ZeroMQ + zmq_events = [] + while len(zmq_events) < len(events_json): + msg = await self.socket.recv() + event_data = json.loads(msg.decode("utf-8")) + zmq_events.append(event_data) + + zmq_events.sort(key=lambda x: x["timestamp"]) + + assert len(events_json) == len(zmq_events), "Number of events does not match" + + # Verify the events match + assert events_json == zmq_events, "Events do not match" + + finally: + # Clean up: Close the ZeroMQ socket + self.socket.close() + self.context.term() diff --git a/bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py b/bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py index bfa186707b..4d1fbff4b3 100644 --- a/bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py +++ b/bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py @@ -54,8 +54,8 @@ def check(self, module_test, events): class TestSubdomainEnumHighestParent(TestSubdomainEnum): - targets = ["api.test.asdf.www.blacklanternsecurity.com", "evilcorp.com"] - whitelist = ["www.blacklanternsecurity.com"] + seeds = ["api.test.asdf.www.blacklanternsecurity.com", "evilcorp.com"] + targets = ["www.blacklanternsecurity.com"] modules_overrides = ["speculate"] dedup_strategy = "highest_parent" txt = None @@ -71,8 +71,11 @@ def check(self, module_test, events): assert len(distance_1_dns_names) == 2 assert 1 == len([e for e in distance_1_dns_names if e.data == "evilcorp.com"]) assert 1 == len([e for e in distance_1_dns_names if e.data == "blacklanternsecurity.com"]) - assert len(self.queries) == 1 - assert self.queries[0] == "www.blacklanternsecurity.com" + + # Passive subdomain enum templates operate on all seeds, even when + # they are outside the explicit target_list. + # we expect one query for the blacklantern scope and one for the unrelated evilcorp.com seed. + assert set(self.queries) == {"www.blacklanternsecurity.com", "evilcorp.com"} class TestSubdomainEnumLowestParent(TestSubdomainEnumHighestParent): @@ -80,6 +83,7 @@ class TestSubdomainEnumLowestParent(TestSubdomainEnumHighestParent): def check(self, module_test, events): assert set(self.queries) == { + "evilcorp.com", "test.asdf.www.blacklanternsecurity.com", "asdf.www.blacklanternsecurity.com", "www.blacklanternsecurity.com", @@ -88,8 +92,8 @@ def check(self, module_test, events): class TestSubdomainEnumWildcardBaseline(ModuleTestBase): # oh walmart.cn why are you like this - targets = ["www.walmart.cn"] - whitelist = ["walmart.cn"] + targets = ["walmart.cn"] + seeds = ["www.walmart.cn"] modules_overrides = [] config_overrides = {"dns": {"minimal": False}, "scope": {"report_distance": 10}, "omit_event_types": []} dedup_strategy = "highest_parent" @@ -101,9 +105,11 @@ class TestSubdomainEnumWildcardBaseline(ModuleTestBase): } async def setup_before_prep(self, module_test): - await module_test.mock_dns(self.dns_mock_data) self.queries = [] + async def setup_after_prep(self, module_test): + await module_test.mock_dns(self.dns_mock_data) + async def mock_query(query): self.queries.append(query) return ["walmart.cn", "www.walmart.cn", "test.walmart.cn", "asdf.walmart.cn"] @@ -112,6 +118,7 @@ async def mock_query(query): from bbot.modules.templates.subdomain_enum import subdomain_enum subdomain_enum_module = subdomain_enum(module_test.scan) + await subdomain_enum_module.setup() subdomain_enum_module.query = mock_query subdomain_enum_module._name = "subdomain_enum" @@ -134,7 +141,7 @@ def check(self, module_test, events): for e in events if e.type == "DNS_NAME" and e.data == "www.walmart.cn" - and str(e.module) == "TARGET" + and str(e.module) == "SEED" and e.scope_distance == 0 ] ) @@ -163,6 +170,7 @@ def check(self, module_test, events): class TestSubdomainEnumWildcardDefense(TestSubdomainEnumWildcardBaseline): # oh walmart.cn why are you like this targets = ["walmart.cn"] + seeds = ["walmart.cn"] modules_overrides = [] config_overrides = {"dns": {"minimal": False}, "scope": {"report_distance": 10}} dedup_strategy = "highest_parent" @@ -195,7 +203,7 @@ def check(self, module_test, events): for e in events if e.type == "DNS_NAME" and e.data == "walmart.cn" - and str(e.module) == "TARGET" + and str(e.module) == "SEED" and e.scope_distance == 0 ] ) diff --git a/docs/data/chord_graph/entities.json b/docs/data/chord_graph/entities.json index 5afb9f73aa..7a0426345c 100644 --- a/docs/data/chord_graph/entities.json +++ b/docs/data/chord_graph/entities.json @@ -23,36 +23,39 @@ ] }, { - "id": 141, + "id": 19, "name": "AZURE_TENANT", "parent": 88888888, "consumes": [ - 140 + 139 ], - "produces": [] + "produces": [ + 18 + ] }, { - "id": 46, + "id": 45, "name": "CODE_REPOSITORY", "parent": 88888888, "consumes": [ - 65, - 84, - 85, + 64, + 81, + 82, + 86, 89, - 92, - 127, - 148 + 125, + 145, + 147 ], "produces": [ - 45, - 66, + 44, + 65, + 80, 83, - 86, + 84, 87, - 90, - 91, - 126 + 88, + 124 ] }, { @@ -62,141 +65,139 @@ "consumes": [ 6, 15, - 19, + 18, 21, - 22, - 26, + 25, + 27, 28, 29, - 30, + 31, 32, 33, 34, 35, - 36, + 37, 38, - 39, + 42, 43, - 44, - 47, + 46, + 51, 52, 53, 54, 55, - 56, + 57, 58, 59, 60, 61, - 62, - 64, - 70, - 81, - 86, - 88, - 96, - 100, - 107, + 63, + 69, + 79, + 83, + 85, + 93, + 97, + 105, + 109, 111, - 113, - 116, - 117, - 121, + 114, + 115, + 119, + 120, 122, - 124, - 128, + 126, + 130, + 131, 132, - 133, 134, 135, 136, - 137, - 140, + 139, + 141, + 142, 143, - 144, - 145, - 147, + 146, + 150, 151, - 154, - 155, - 157 + 152, + 154 ], "produces": [ 6, - 21, - 28, + 18, + 27, + 34, 35, - 36, + 37, 38, 39, - 40, + 42, 43, - 44, + 51, 52, - 53, - 55, + 54, + 57, 58, 59, 60, 61, 62, - 63, - 81, - 96, - 100, - 107, - 111, + 79, + 93, + 97, + 105, + 109, + 112, 114, - 116, - 117, - 121, - 128, + 115, + 119, + 126, + 130, 132, 134, 135, - 136, + 139, 140, + 141, 142, - 143, - 144, - 147, + 146, + 150, 151, 152, - 154, - 155, - 157 + 154 ] }, { - "id": 23, + "id": 22, "name": "DNS_NAME_UNRESOLVED", "parent": 88888888, "consumes": [ - 22, - 140, - 145 + 21, + 139, + 143 ], "produces": [] }, { - "id": 48, + "id": 47, "name": "EMAIL_ADDRESS", "parent": 88888888, "consumes": [ - 71 + 70 ], "produces": [ - 47, - 54, - 60, - 64, - 70, - 88, - 100, - 122, - 133, - 137, - 142 + 46, + 53, + 59, + 63, + 69, + 85, + 97, + 120, + 131, + 136, + 140 ] }, { @@ -204,21 +205,21 @@ "name": "FILESYSTEM", "parent": 88888888, "consumes": [ - 75, - 106, - 148, - 149 + 103, + 104, + 147, + 148 ], "produces": [ 8, - 65, - 79, - 84, - 85, - 89, - 106, - 127, - 149 + 64, + 77, + 81, + 82, + 86, + 103, + 125, + 148 ] }, { @@ -227,61 +228,65 @@ "parent": 88888888, "consumes": [ 15, - 159 + 156 ], "produces": [ 1, - 22, - 24, + 14, + 18, + 21, + 23, + 25, 26, - 27, + 28, 29, - 30, + 31, 32, 33, - 34, - 37, - 83, - 91, - 95, - 97, - 99, + 36, + 68, + 80, + 88, + 92, + 94, + 96, + 106, + 107, 108, - 109, + 110, 112, - 114, - 115, - 118, - 119, - 129, - 130, - 135, - 138, - 140, - 146, - 148, - 150, - 160 + 113, + 127, + 128, + 133, + 134, + 137, + 139, + 144, + 145, + 147, + 149, + 158 ] }, { - "id": 103, + "id": 100, "name": "GEOLOCATION", "parent": 88888888, "consumes": [], "produces": [ - 102, - 105 + 99, + 102 ] }, { - "id": 49, + "id": 48, "name": "HASHED_PASSWORD", "parent": 88888888, "consumes": [], "produces": [ - 47, - 54 + 46, + 53 ] }, { @@ -291,25 +296,25 @@ "consumes": [ 1, 15, - 27, - 69, - 72, - 79, - 91, - 97, + 26, + 68, + 71, + 77, + 88, + 94, + 110, + 111, 112, - 113, - 114, + 116, + 117, 118, - 119, - 120, - 140, - 146, - 148, - 160 + 139, + 144, + 147, + 158 ], "produces": [ - 98 + 95 ] }, { @@ -319,30 +324,31 @@ "consumes": [ 11, 15, - 40, + 39, + 99, + 101, 102, - 104, - 105, - 113, - 124, - 135, - 140 + 111, + 122, + 133, + 134, + 139 ], "produces": [ 15, - 40, - 63, - 104, - 140 + 39, + 62, + 101, + 139 ] }, { - "id": 125, + "id": 123, "name": "IP_RANGE", "parent": 88888888, "consumes": [ - 124, - 140 + 122, + 139 ], "produces": [] }, @@ -354,7 +360,7 @@ 8 ], "produces": [ - 92 + 89 ] }, { @@ -363,151 +369,156 @@ "parent": 88888888, "consumes": [ 15, - 80, - 98, - 113, - 123, - 142 + 78, + 95, + 111, + 121, + 140 ], "produces": [ 15, - 40, - 124, - 135, - 140 + 39, + 122, + 133, + 134, + 139 ] }, { - "id": 41, + "id": 40, "name": "OPEN_UDP_PORT", "parent": 88888888, "consumes": [], "produces": [ - 40 + 39, + 133 ] }, { - "id": 67, + "id": 66, "name": "ORG_STUB", "parent": 88888888, "consumes": [ - 66, - 87, - 92, - 126 + 65, + 84, + 89, + 124 ], "produces": [ - 140 + 139 ] }, { - "id": 50, + "id": 49, "name": "PASSWORD", "parent": 88888888, "consumes": [], "produces": [ - 47, - 54 + 46, + 53 ] }, { - "id": 42, + "id": 41, "name": "PROTOCOL", "parent": 88888888, "consumes": [ + 106, 108, - 110, - 113 + 111 ], "produces": [ - 40, - 80 + 39, + 78 ] }, { - "id": 57, + "id": 56, "name": "RAW_DNS_RECORD", "parent": 88888888, "consumes": [], "produces": [ - 56, - 63, - 64 + 55, + 62, + 63 ] }, { - "id": 73, + "id": 72, "name": "RAW_TEXT", "parent": 88888888, "consumes": [ - 72, - 148 + 71, + 147 ], "produces": [ - 75 + 104 ] }, { - "id": 68, + "id": 67, "name": "SOCIAL", "parent": 88888888, "consumes": [ - 66, + 65, + 84, 87, + 88, 90, - 91, - 93, - 126, - 140 + 124, + 139 ], "produces": [ - 66, + 65, + 85, 88, - 91, - 139 + 138 ] }, { - "id": 25, + "id": 24, "name": "STORAGE_BUCKET", "parent": 88888888, "consumes": [ - 24, + 23, + 28, 29, 30, 31, 32, 33, - 34, - 140 + 139 ], "produces": [ + 28, 29, - 30, + 31, 32, - 33, - 34 + 33 ] }, { - "id": 17, + "id": 5, "name": "TECHNOLOGY", "parent": 88888888, "consumes": [ 15, - 91, - 159, - 160 + 88, + 145, + 156, + 158 ], "produces": [ - 27, - 40, - 69, - 91, - 93, - 115, - 135, - 160 + 1, + 26, + 39, + 68, + 88, + 90, + 113, + 133, + 134, + 158 ] }, { @@ -518,167 +529,138 @@ 1, 14, 15, - 24, - 37, - 76, - 82, - 83, - 93, + 23, + 36, + 74, + 80, + 90, + 92, 95, 98, - 101, - 109, - 114, - 115, - 123, - 131, - 138, - 140, - 146, - 150, - 152, - 156, - 159 + 107, + 112, + 113, + 121, + 129, + 137, + 139, + 144, + 149, + 153, + 156 ], "produces": [ - 93, - 98 + 90, + 95 ] }, { - "id": 78, + "id": 76, "name": "URL_HINT", "parent": 88888888, "consumes": [ - 77 + 75 ], "produces": [ - 101 + 98 ] }, { "id": 20, - "name": "URL_UNVERIFIED", - "parent": 88888888, - "consumes": [ - 45, - 79, - 98, - 116, - 123, - 130, - 139, - 140 - ], - "produces": [ - 19, - 28, - 31, - 40, - 56, - 60, - 64, - 66, - 72, - 76, - 77, - 86, - 93, - 100, - 131, - 133, - 151, - 157, - 160 - ] - }, - { - "id": 51, - "name": "USERNAME", + "name": "URL_UNVERIFIED", "parent": 88888888, "consumes": [ - 140 + 44, + 77, + 95, + 114, + 121, + 128, + 138, + 139, + 145 ], "produces": [ - 47, - 54 + 18, + 27, + 30, + 39, + 55, + 59, + 63, + 65, + 71, + 74, + 75, + 83, + 90, + 97, + 129, + 131, + 150, + 154, + 158 ] }, { - "id": 153, - "name": "VHOST", + "id": 50, + "name": "USERNAME", "parent": 88888888, "consumes": [ - 159 + 139 ], "produces": [ - 152 + 46, + 53 ] }, { - "id": 5, - "name": "VULNERABILITY", + "id": 157, + "name": "VHOST", "parent": 88888888, "consumes": [ - 15, - 159 + 156 ], - "produces": [ - 1, - 14, - 22, - 24, - 26, - 27, - 69, - 82, - 109, - 110, - 115, - 135, - 146, - 148, - 160 - ] + "produces": [] }, { - "id": 18, + "id": 17, "name": "WAF", "parent": 88888888, "consumes": [ 15 ], "produces": [ - 156 + 153 ] }, { - "id": 94, + "id": 91, "name": "WEBSCREENSHOT", "parent": 88888888, "consumes": [], "produces": [ - 93 + 90 ] }, { - "id": 74, + "id": 73, "name": "WEB_PARAMETER", "parent": 88888888, "consumes": [ - 99, - 109, + 96, + 107, + 116, + 117, 118, - 119, - 120, - 129, - 158 + 127, + 155 ], "produces": [ - 72, - 118, - 119, - 120 + 71, + 116, + 117, + 118 ] }, { @@ -735,7 +717,7 @@ 3 ], "produces": [ - 5 + 4 ] }, { @@ -748,10 +730,9 @@ 2, 12, 16, - 17, - 3, 5, - 18 + 3, + 17 ], "produces": [ 12, @@ -759,67 +740,56 @@ ] }, { - "id": 19, - "name": "azure_realm", + "id": 18, + "name": "azure_tenant", "parent": 99999999, "consumes": [ 7 ], "produces": [ + 19, + 7, + 4, 20 ] }, { "id": 21, - "name": "azure_tenant", - "parent": 99999999, - "consumes": [ - 7 - ], - "produces": [ - 7 - ] - }, - { - "id": 22, "name": "baddns", "parent": 99999999, "consumes": [ 7, - 23 + 22 ], "produces": [ - 4, - 5 + 4 ] }, { - "id": 24, + "id": 23, "name": "baddns_direct", "parent": 99999999, "consumes": [ - 25, + 24, 3 ], "produces": [ - 4, - 5 + 4 ] }, { - "id": 26, + "id": 25, "name": "baddns_zone", "parent": 99999999, "consumes": [ 7 ], "produces": [ - 4, - 5 + 4 ] }, { - "id": 27, + "id": 26, "name": "badsecrets", "parent": 99999999, "consumes": [ @@ -827,12 +797,11 @@ ], "produces": [ 4, - 17, 5 ] }, { - "id": 28, + "id": 27, "name": "bevigil", "parent": 99999999, "consumes": [ @@ -844,83 +813,83 @@ ] }, { - "id": 29, + "id": 28, "name": "bucket_amazon", "parent": 99999999, "consumes": [ 7, - 25 + 24 ], "produces": [ 4, - 25 + 24 ] }, { - "id": 30, + "id": 29, "name": "bucket_digitalocean", "parent": 99999999, "consumes": [ 7, - 25 + 24 ], "produces": [ 4, - 25 + 24 ] }, { - "id": 31, + "id": 30, "name": "bucket_file_enum", "parent": 99999999, "consumes": [ - 25 + 24 ], "produces": [ 20 ] }, { - "id": 32, + "id": 31, "name": "bucket_firebase", "parent": 99999999, "consumes": [ 7, - 25 + 24 ], "produces": [ 4, - 25 + 24 ] }, { - "id": 33, + "id": 32, "name": "bucket_google", "parent": 99999999, "consumes": [ 7, - 25 + 24 ], "produces": [ 4, - 25 + 24 ] }, { - "id": 34, + "id": 33, "name": "bucket_microsoft", "parent": 99999999, "consumes": [ 7, - 25 + 24 ], "produces": [ 4, - 25 + 24 ] }, { - "id": 35, + "id": 34, "name": "bufferoverrun", "parent": 99999999, "consumes": [ @@ -931,7 +900,7 @@ ] }, { - "id": 36, + "id": 35, "name": "builtwith", "parent": 99999999, "consumes": [ @@ -942,7 +911,7 @@ ] }, { - "id": 37, + "id": 36, "name": "bypass403", "parent": 99999999, "consumes": [ @@ -953,7 +922,7 @@ ] }, { - "id": 38, + "id": 37, "name": "c99", "parent": 99999999, "consumes": [ @@ -964,7 +933,7 @@ ] }, { - "id": 39, + "id": 38, "name": "censys_dns", "parent": 99999999, "consumes": [ @@ -975,7 +944,7 @@ ] }, { - "id": 40, + "id": 39, "name": "censys_ip", "parent": 99999999, "consumes": [ @@ -985,14 +954,14 @@ 7, 12, 16, + 40, 41, - 42, - 17, + 5, 20 ] }, { - "id": 43, + "id": 42, "name": "certspotter", "parent": 99999999, "consumes": [ @@ -1003,7 +972,7 @@ ] }, { - "id": 44, + "id": 43, "name": "chaos", "parent": 99999999, "consumes": [ @@ -1014,32 +983,32 @@ ] }, { - "id": 45, + "id": 44, "name": "code_repository", "parent": 99999999, "consumes": [ 20 ], "produces": [ - 46 + 45 ] }, { - "id": 47, + "id": 46, "name": "credshed", "parent": 99999999, "consumes": [ 7 ], "produces": [ + 47, 48, 49, - 50, - 51 + 50 ] }, { - "id": 52, + "id": 51, "name": "crt", "parent": 99999999, "consumes": [ @@ -1050,7 +1019,7 @@ ] }, { - "id": 53, + "id": 52, "name": "crt_db", "parent": 99999999, "consumes": [ @@ -1061,21 +1030,21 @@ ] }, { - "id": 54, + "id": 53, "name": "dehashed", "parent": 99999999, "consumes": [ 7 ], "produces": [ + 47, 48, 49, - 50, - 51 + 50 ] }, { - "id": 55, + "id": 54, "name": "digitorus", "parent": 99999999, "consumes": [ @@ -1086,19 +1055,19 @@ ] }, { - "id": 56, + "id": 55, "name": "dnsbimi", "parent": 99999999, "consumes": [ 7 ], "produces": [ - 57, + 56, 20 ] }, { - "id": 58, + "id": 57, "name": "dnsbrute", "parent": 99999999, "consumes": [ @@ -1109,7 +1078,7 @@ ] }, { - "id": 59, + "id": 58, "name": "dnsbrute_mutations", "parent": 99999999, "consumes": [ @@ -1120,7 +1089,7 @@ ] }, { - "id": 60, + "id": 59, "name": "dnscaa", "parent": 99999999, "consumes": [ @@ -1128,12 +1097,12 @@ ], "produces": [ 7, - 48, + 47, 20 ] }, { - "id": 61, + "id": 60, "name": "dnscommonsrv", "parent": 99999999, "consumes": [ @@ -1144,7 +1113,7 @@ ] }, { - "id": 62, + "id": 61, "name": "dnsdumpster", "parent": 99999999, "consumes": [ @@ -1155,112 +1124,101 @@ ] }, { - "id": 63, + "id": 62, "name": "dnsresolve", "parent": 99999999, "consumes": [], "produces": [ 7, 12, - 57 + 56 ] }, { - "id": 64, + "id": 63, "name": "dnstlsrpt", "parent": 99999999, "consumes": [ 7 ], "produces": [ - 48, - 57, + 47, + 56, 20 ] }, { - "id": 65, + "id": 64, "name": "docker_pull", "parent": 99999999, "consumes": [ - 46 + 45 ], "produces": [ 10 ] }, { - "id": 66, + "id": 65, "name": "dockerhub", "parent": 99999999, "consumes": [ - 67, - 68 + 66, + 67 ], "produces": [ - 46, - 68, + 45, + 67, 20 ] }, { - "id": 69, + "id": 68, "name": "dotnetnuke", "parent": 99999999, "consumes": [ 2 ], "produces": [ - 17, + 4, 5 ] }, { - "id": 70, + "id": 69, "name": "emailformat", "parent": 99999999, "consumes": [ 7 ], "produces": [ - 48 + 47 ] }, { - "id": 71, + "id": 70, "name": "emails", "parent": 99999999, "consumes": [ - 48 + 47 ], "produces": [] }, { - "id": 72, + "id": 71, "name": "excavate", "parent": 99999999, "consumes": [ 2, - 73 + 72 ], "produces": [ 20, - 74 - ] - }, - { - "id": 75, - "name": "extractous", - "parent": 99999999, - "consumes": [ - 10 - ], - "produces": [ 73 ] }, { - "id": 76, + "id": 74, "name": "ffuf", "parent": 99999999, "consumes": [ @@ -1271,18 +1229,18 @@ ] }, { - "id": 77, + "id": 75, "name": "ffuf_shortnames", "parent": 99999999, "consumes": [ - 78 + 76 ], "produces": [ 20 ] }, { - "id": 79, + "id": 77, "name": "filedownload", "parent": 99999999, "consumes": [ @@ -1294,18 +1252,18 @@ ] }, { - "id": 80, + "id": 78, "name": "fingerprintx", "parent": 99999999, "consumes": [ 16 ], "produces": [ - 42 + 41 ] }, { - "id": 81, + "id": 79, "name": "fullhunt", "parent": 99999999, "consumes": [ @@ -1316,153 +1274,142 @@ ] }, { - "id": 82, - "name": "generic_ssrf", - "parent": 99999999, - "consumes": [ - 3 - ], - "produces": [ - 5 - ] - }, - { - "id": 83, + "id": 80, "name": "git", "parent": 99999999, "consumes": [ 3 ], "produces": [ - 46, + 45, 4 ] }, { - "id": 84, + "id": 81, "name": "git_clone", "parent": 99999999, "consumes": [ - 46 + 45 ], "produces": [ 10 ] }, { - "id": 85, + "id": 82, "name": "gitdumper", "parent": 99999999, "consumes": [ - 46 + 45 ], "produces": [ 10 ] }, { - "id": 86, + "id": 83, "name": "github_codesearch", "parent": 99999999, "consumes": [ 7 ], "produces": [ - 46, + 45, 20 ] }, { - "id": 87, + "id": 84, "name": "github_org", "parent": 99999999, "consumes": [ - 67, - 68 + 66, + 67 ], "produces": [ - 46 + 45 ] }, { - "id": 88, + "id": 85, "name": "github_usersearch", "parent": 99999999, "consumes": [ 7 ], "produces": [ - 48, - 68 + 47, + 67 ] }, { - "id": 89, + "id": 86, "name": "github_workflows", "parent": 99999999, "consumes": [ - 46 + 45 ], "produces": [ 10 ] }, { - "id": 90, + "id": 87, "name": "gitlab_com", "parent": 99999999, "consumes": [ - 68 + 67 ], "produces": [ - 46 + 45 ] }, { - "id": 91, + "id": 88, "name": "gitlab_onprem", "parent": 99999999, "consumes": [ 2, - 68, - 17 + 67, + 5 ], "produces": [ - 46, + 45, 4, - 68, - 17 + 67, + 5 ] }, { - "id": 92, + "id": 89, "name": "google_playstore", "parent": 99999999, "consumes": [ - 46, - 67 + 45, + 66 ], "produces": [ 9 ] }, { - "id": 93, + "id": 90, "name": "gowitness", "parent": 99999999, "consumes": [ - 68, + 67, 3 ], "produces": [ - 17, + 5, 3, 20, - 94 + 91 ] }, { - "id": 95, + "id": 92, "name": "graphql_introspection", "parent": 99999999, "consumes": [ @@ -1473,7 +1420,7 @@ ] }, { - "id": 96, + "id": 93, "name": "hackertarget", "parent": 99999999, "consumes": [ @@ -1484,7 +1431,7 @@ ] }, { - "id": 97, + "id": 94, "name": "host_header", "parent": 99999999, "consumes": [ @@ -1495,7 +1442,7 @@ ] }, { - "id": 98, + "id": 95, "name": "httpx", "parent": 99999999, "consumes": [ @@ -1509,18 +1456,18 @@ ] }, { - "id": 99, + "id": 96, "name": "hunt", "parent": 99999999, "consumes": [ - 74 + 73 ], "produces": [ 4 ] }, { - "id": 100, + "id": 97, "name": "hunterio", "parent": 99999999, "consumes": [ @@ -1528,34 +1475,34 @@ ], "produces": [ 7, - 48, + 47, 20 ] }, { - "id": 101, + "id": 98, "name": "iis_shortnames", "parent": 99999999, "consumes": [ 3 ], "produces": [ - 78 + 76 ] }, { - "id": 102, + "id": 99, "name": "ip2location", "parent": 99999999, "consumes": [ 12 ], "produces": [ - 103 + 100 ] }, { - "id": 104, + "id": 101, "name": "ipneighbor", "parent": 99999999, "consumes": [ @@ -1566,18 +1513,18 @@ ] }, { - "id": 105, + "id": 102, "name": "ipstack", "parent": 99999999, "consumes": [ 12 ], "produces": [ - 103 + 100 ] }, { - "id": 106, + "id": 103, "name": "jadx", "parent": 99999999, "consumes": [ @@ -1588,7 +1535,18 @@ ] }, { - "id": 107, + "id": 104, + "name": "kreuzberg", + "parent": 99999999, + "consumes": [ + 10 + ], + "produces": [ + 72 + ] + }, + { + "id": 105, "name": "leakix", "parent": 99999999, "consumes": [ @@ -1599,42 +1557,41 @@ ] }, { - "id": 108, + "id": 106, "name": "legba", "parent": 99999999, "consumes": [ - 42 + 41 ], "produces": [ 4 ] }, { - "id": 109, + "id": 107, "name": "lightfuzz", "parent": 99999999, "consumes": [ 3, - 74 + 73 ], "produces": [ - 4, - 5 + 4 ] }, { - "id": 110, + "id": 108, "name": "medusa", "parent": 99999999, "consumes": [ - 42 + 41 ], "produces": [ - 5 + 4 ] }, { - "id": 111, + "id": 109, "name": "myssl", "parent": 99999999, "consumes": [ @@ -1645,7 +1602,7 @@ ] }, { - "id": 112, + "id": 110, "name": "newsletters", "parent": 99999999, "consumes": [ @@ -1656,7 +1613,7 @@ ] }, { - "id": 113, + "id": 111, "name": "nmap_xml", "parent": 99999999, "consumes": [ @@ -1664,12 +1621,12 @@ 2, 12, 16, - 42 + 41 ], "produces": [] }, { - "id": 114, + "id": 112, "name": "ntlm", "parent": 99999999, "consumes": [ @@ -1682,7 +1639,7 @@ ] }, { - "id": 115, + "id": 113, "name": "nuclei", "parent": 99999999, "consumes": [ @@ -1690,12 +1647,11 @@ ], "produces": [ 4, - 17, 5 ] }, { - "id": 116, + "id": 114, "name": "oauth", "parent": 99999999, "consumes": [ @@ -1707,7 +1663,7 @@ ] }, { - "id": 117, + "id": 115, "name": "otx", "parent": 99999999, "consumes": [ @@ -1718,45 +1674,43 @@ ] }, { - "id": 118, + "id": 116, "name": "paramminer_cookies", "parent": 99999999, "consumes": [ 2, - 74 + 73 ], "produces": [ - 4, - 74 + 73 ] }, { - "id": 119, + "id": 117, "name": "paramminer_getparams", "parent": 99999999, "consumes": [ 2, - 74 + 73 ], "produces": [ - 4, - 74 + 73 ] }, { - "id": 120, + "id": 118, "name": "paramminer_headers", "parent": 99999999, "consumes": [ 2, - 74 + 73 ], "produces": [ - 74 + 73 ] }, { - "id": 121, + "id": 119, "name": "passivetotal", "parent": 99999999, "consumes": [ @@ -1767,18 +1721,18 @@ ] }, { - "id": 122, + "id": 120, "name": "pgp", "parent": 99999999, "consumes": [ 7 ], "produces": [ - 48 + 47 ] }, { - "id": 123, + "id": 121, "name": "portfilter", "parent": 99999999, "consumes": [ @@ -1789,43 +1743,43 @@ "produces": [] }, { - "id": 124, + "id": 122, "name": "portscan", "parent": 99999999, "consumes": [ 7, 12, - 125 + 123 ], "produces": [ 16 ] }, { - "id": 126, + "id": 124, "name": "postman", "parent": 99999999, "consumes": [ - 67, - 68 + 66, + 67 ], "produces": [ - 46 + 45 ] }, { - "id": 127, + "id": 125, "name": "postman_download", "parent": 99999999, "consumes": [ - 46 + 45 ], "produces": [ 10 ] }, { - "id": 128, + "id": 126, "name": "rapiddns", "parent": 99999999, "consumes": [ @@ -1836,18 +1790,18 @@ ] }, { - "id": 129, + "id": 127, "name": "reflected_parameters", "parent": 99999999, "consumes": [ - 74 + 73 ], "produces": [ 4 ] }, { - "id": 130, + "id": 128, "name": "retirejs", "parent": 99999999, "consumes": [ @@ -1858,7 +1812,7 @@ ] }, { - "id": 131, + "id": 129, "name": "robots", "parent": 99999999, "consumes": [ @@ -1869,7 +1823,7 @@ ] }, { - "id": 132, + "id": 130, "name": "securitytrails", "parent": 99999999, "consumes": [ @@ -1880,19 +1834,19 @@ ] }, { - "id": 133, + "id": 131, "name": "securitytxt", "parent": 99999999, "consumes": [ 7 ], "produces": [ - 48, + 47, 20 ] }, { - "id": 134, + "id": 132, "name": "shodan_dns", "parent": 99999999, "consumes": [ @@ -1903,7 +1857,21 @@ ] }, { - "id": 135, + "id": 133, + "name": "shodan_enterprise", + "parent": 99999999, + "consumes": [ + 12 + ], + "produces": [ + 4, + 16, + 40, + 5 + ] + }, + { + "id": 134, "name": "shodan_idb", "parent": 99999999, "consumes": [ @@ -1914,12 +1882,11 @@ 7, 4, 16, - 17, 5 ] }, { - "id": 136, + "id": 135, "name": "sitedossier", "parent": 99999999, "consumes": [ @@ -1930,18 +1897,18 @@ ] }, { - "id": 137, + "id": 136, "name": "skymem", "parent": 99999999, "consumes": [ 7 ], "produces": [ - 48 + 47 ] }, { - "id": 138, + "id": 137, "name": "smuggler", "parent": 99999999, "consumes": [ @@ -1952,43 +1919,43 @@ ] }, { - "id": 139, + "id": 138, "name": "social", "parent": 99999999, "consumes": [ 20 ], "produces": [ - 68 + 67 ] }, { - "id": 140, + "id": 139, "name": "speculate", "parent": 99999999, "consumes": [ - 141, + 19, 7, - 23, + 22, 2, 12, - 125, - 68, - 25, + 123, + 67, + 24, 3, 20, - 51 + 50 ], "produces": [ 7, 4, 12, 16, - 67 + 66 ] }, { - "id": 142, + "id": 140, "name": "sslcert", "parent": 99999999, "consumes": [ @@ -1996,11 +1963,11 @@ ], "produces": [ 7, - 48 + 47 ] }, { - "id": 143, + "id": 141, "name": "subdomaincenter", "parent": 99999999, "consumes": [ @@ -2011,7 +1978,7 @@ ] }, { - "id": 144, + "id": 142, "name": "subdomainradar", "parent": 99999999, "consumes": [ @@ -2022,17 +1989,17 @@ ] }, { - "id": 145, + "id": 143, "name": "subdomains", "parent": 99999999, "consumes": [ 7, - 23 + 22 ], "produces": [] }, { - "id": 146, + "id": 144, "name": "telerik", "parent": 99999999, "consumes": [ @@ -2040,12 +2007,24 @@ 3 ], "produces": [ - 4, - 5 + 4 ] }, { - "id": 147, + "id": 145, + "name": "trajan", + "parent": 99999999, + "consumes": [ + 45, + 5, + 20 + ], + "produces": [ + 4 + ] + }, + { + "id": 146, "name": "trickest", "parent": 99999999, "consumes": [ @@ -2056,22 +2035,21 @@ ] }, { - "id": 148, + "id": 147, "name": "trufflehog", "parent": 99999999, "consumes": [ - 46, + 45, 10, 2, - 73 + 72 ], "produces": [ - 4, - 5 + 4 ] }, { - "id": 149, + "id": 148, "name": "unarchive", "parent": 99999999, "consumes": [ @@ -2082,7 +2060,7 @@ ] }, { - "id": 150, + "id": 149, "name": "url_manipulation", "parent": 99999999, "consumes": [ @@ -2093,7 +2071,7 @@ ] }, { - "id": 151, + "id": 150, "name": "urlscan", "parent": 99999999, "consumes": [ @@ -2105,19 +2083,7 @@ ] }, { - "id": 152, - "name": "vhost", - "parent": 99999999, - "consumes": [ - 3 - ], - "produces": [ - 7, - 153 - ] - }, - { - "id": 154, + "id": 151, "name": "viewdns", "parent": 99999999, "consumes": [ @@ -2128,7 +2094,7 @@ ] }, { - "id": 155, + "id": 152, "name": "virustotal", "parent": 99999999, "consumes": [ @@ -2139,18 +2105,18 @@ ] }, { - "id": 156, + "id": 153, "name": "wafw00f", "parent": 99999999, "consumes": [ 3 ], "produces": [ - 18 + 17 ] }, { - "id": 157, + "id": 154, "name": "wayback", "parent": 99999999, "consumes": [ @@ -2162,40 +2128,38 @@ ] }, { - "id": 158, + "id": 155, "name": "web_parameters", "parent": 99999999, "consumes": [ - 74 + 73 ], "produces": [] }, { - "id": 159, + "id": 156, "name": "web_report", "parent": 99999999, "consumes": [ 4, - 17, + 5, 3, - 153, - 5 + 157 ], "produces": [] }, { - "id": 160, + "id": 158, "name": "wpscan", "parent": 99999999, "consumes": [ 2, - 17 + 5 ], "produces": [ 4, - 17, - 20, - 5 + 5, + 20 ] } ] \ No newline at end of file diff --git a/docs/data/chord_graph/rels.json b/docs/data/chord_graph/rels.json index 176e46f0a0..f20fef2f11 100644 --- a/docs/data/chord_graph/rels.json +++ b/docs/data/chord_graph/rels.json @@ -55,7 +55,7 @@ "type": "consumes" }, { - "source": 5, + "source": 4, "target": 14, "type": "produces" }, @@ -86,7 +86,7 @@ }, { "source": 15, - "target": 17, + "target": 5, "type": "consumes" }, { @@ -96,12 +96,7 @@ }, { "source": 15, - "target": 5, - "type": "consumes" - }, - { - "source": 15, - "target": 18, + "target": 17, "type": "consumes" }, { @@ -115,68 +110,73 @@ "type": "produces" }, { - "source": 19, + "source": 18, "target": 7, "type": "consumes" }, { - "source": 20, - "target": 19, + "source": 19, + "target": 18, "type": "produces" }, { - "source": 21, - "target": 7, - "type": "consumes" + "source": 7, + "target": 18, + "type": "produces" }, { - "source": 7, - "target": 21, + "source": 4, + "target": 18, "type": "produces" }, { - "source": 22, - "target": 7, - "type": "consumes" + "source": 20, + "target": 18, + "type": "produces" }, { - "source": 22, - "target": 23, + "source": 21, + "target": 7, "type": "consumes" }, { - "source": 4, + "source": 21, "target": 22, - "type": "produces" + "type": "consumes" }, { - "source": 5, - "target": 22, + "source": 4, + "target": 21, "type": "produces" }, { - "source": 24, - "target": 25, + "source": 23, + "target": 24, "type": "consumes" }, { - "source": 24, + "source": 23, "target": 3, "type": "consumes" }, { "source": 4, - "target": 24, + "target": 23, "type": "produces" }, { - "source": 5, - "target": 24, + "source": 25, + "target": 7, + "type": "consumes" + }, + { + "source": 4, + "target": 25, "type": "produces" }, { "source": 26, - "target": 7, + "target": 2, "type": "consumes" }, { @@ -191,36 +191,36 @@ }, { "source": 27, - "target": 2, + "target": 7, "type": "consumes" }, { - "source": 4, + "source": 7, "target": 27, "type": "produces" }, { - "source": 17, + "source": 20, "target": 27, "type": "produces" }, { - "source": 5, - "target": 27, - "type": "produces" + "source": 28, + "target": 7, + "type": "consumes" }, { "source": 28, - "target": 7, + "target": 24, "type": "consumes" }, { - "source": 7, + "source": 4, "target": 28, "type": "produces" }, { - "source": 20, + "source": 24, "target": 28, "type": "produces" }, @@ -231,7 +231,7 @@ }, { "source": 29, - "target": 25, + "target": 24, "type": "consumes" }, { @@ -240,37 +240,37 @@ "type": "produces" }, { - "source": 25, + "source": 24, "target": 29, "type": "produces" }, { "source": 30, - "target": 7, - "type": "consumes" - }, - { - "source": 30, - "target": 25, + "target": 24, "type": "consumes" }, { - "source": 4, + "source": 20, "target": 30, "type": "produces" }, { - "source": 25, - "target": 30, - "type": "produces" + "source": 31, + "target": 7, + "type": "consumes" }, { "source": 31, - "target": 25, + "target": 24, "type": "consumes" }, { - "source": 20, + "source": 4, + "target": 31, + "type": "produces" + }, + { + "source": 24, "target": 31, "type": "produces" }, @@ -281,7 +281,7 @@ }, { "source": 32, - "target": 25, + "target": 24, "type": "consumes" }, { @@ -290,7 +290,7 @@ "type": "produces" }, { - "source": 25, + "source": 24, "target": 32, "type": "produces" }, @@ -301,7 +301,7 @@ }, { "source": 33, - "target": 25, + "target": 24, "type": "consumes" }, { @@ -310,7 +310,7 @@ "type": "produces" }, { - "source": 25, + "source": 24, "target": 33, "type": "produces" }, @@ -320,17 +320,7 @@ "type": "consumes" }, { - "source": 34, - "target": 25, - "type": "consumes" - }, - { - "source": 4, - "target": 34, - "type": "produces" - }, - { - "source": 25, + "source": 7, "target": 34, "type": "produces" }, @@ -346,21 +336,21 @@ }, { "source": 36, - "target": 7, + "target": 3, "type": "consumes" }, { - "source": 7, + "source": 4, "target": 36, "type": "produces" }, { "source": 37, - "target": 3, + "target": 7, "type": "consumes" }, { - "source": 4, + "source": 7, "target": 37, "type": "produces" }, @@ -376,107 +366,107 @@ }, { "source": 39, - "target": 7, - "type": "consumes" - }, - { - "source": 7, - "target": 39, - "type": "produces" - }, - { - "source": 40, "target": 12, "type": "consumes" }, { "source": 7, - "target": 40, + "target": 39, "type": "produces" }, { "source": 12, - "target": 40, + "target": 39, "type": "produces" }, { "source": 16, - "target": 40, + "target": 39, "type": "produces" }, { - "source": 41, - "target": 40, + "source": 40, + "target": 39, "type": "produces" }, { - "source": 42, - "target": 40, + "source": 41, + "target": 39, "type": "produces" }, { - "source": 17, - "target": 40, + "source": 5, + "target": 39, "type": "produces" }, { "source": 20, - "target": 40, + "target": 39, "type": "produces" }, { - "source": 43, + "source": 42, "target": 7, "type": "consumes" }, { "source": 7, - "target": 43, + "target": 42, "type": "produces" }, { - "source": 44, + "source": 43, "target": 7, "type": "consumes" }, { "source": 7, - "target": 44, + "target": 43, "type": "produces" }, { - "source": 45, + "source": 44, "target": 20, "type": "consumes" }, { - "source": 46, - "target": 45, + "source": 45, + "target": 44, "type": "produces" }, { - "source": 47, + "source": 46, "target": 7, "type": "consumes" }, + { + "source": 47, + "target": 46, + "type": "produces" + }, { "source": 48, - "target": 47, + "target": 46, "type": "produces" }, { "source": 49, - "target": 47, + "target": 46, "type": "produces" }, { "source": 50, - "target": 47, + "target": 46, "type": "produces" }, { "source": 51, - "target": 47, + "target": 7, + "type": "consumes" + }, + { + "source": 7, + "target": 51, "type": "produces" }, { @@ -495,960 +485,945 @@ "type": "consumes" }, { - "source": 7, + "source": 47, "target": 53, "type": "produces" }, - { - "source": 54, - "target": 7, - "type": "consumes" - }, { "source": 48, - "target": 54, + "target": 53, "type": "produces" }, { "source": 49, - "target": 54, + "target": 53, "type": "produces" }, { "source": 50, - "target": 54, - "type": "produces" - }, - { - "source": 51, - "target": 54, + "target": 53, "type": "produces" }, { - "source": 55, + "source": 54, "target": 7, "type": "consumes" }, { "source": 7, - "target": 55, + "target": 54, "type": "produces" }, { - "source": 56, + "source": 55, "target": 7, "type": "consumes" }, { - "source": 57, - "target": 56, + "source": 56, + "target": 55, "type": "produces" }, { "source": 20, - "target": 56, + "target": 55, "type": "produces" }, { - "source": 58, + "source": 57, "target": 7, "type": "consumes" }, { "source": 7, - "target": 58, + "target": 57, "type": "produces" }, { - "source": 59, + "source": 58, "target": 7, "type": "consumes" }, { "source": 7, - "target": 59, + "target": 58, "type": "produces" }, { - "source": 60, + "source": 59, "target": 7, "type": "consumes" }, { "source": 7, - "target": 60, + "target": 59, "type": "produces" }, { - "source": 48, - "target": 60, + "source": 47, + "target": 59, "type": "produces" }, { "source": 20, - "target": 60, + "target": 59, "type": "produces" }, { - "source": 61, + "source": 60, "target": 7, "type": "consumes" }, { "source": 7, - "target": 61, + "target": 60, "type": "produces" }, { - "source": 62, + "source": 61, "target": 7, "type": "consumes" }, { "source": 7, - "target": 62, + "target": 61, "type": "produces" }, { "source": 7, - "target": 63, + "target": 62, "type": "produces" }, { "source": 12, - "target": 63, + "target": 62, "type": "produces" }, { - "source": 57, - "target": 63, + "source": 56, + "target": 62, "type": "produces" }, { - "source": 64, + "source": 63, "target": 7, "type": "consumes" }, { - "source": 48, - "target": 64, + "source": 47, + "target": 63, "type": "produces" }, { - "source": 57, - "target": 64, + "source": 56, + "target": 63, "type": "produces" }, { "source": 20, - "target": 64, + "target": 63, "type": "produces" }, { - "source": 65, - "target": 46, + "source": 64, + "target": 45, "type": "consumes" }, { "source": 10, - "target": 65, + "target": 64, "type": "produces" }, { - "source": 66, - "target": 67, + "source": 65, + "target": 66, "type": "consumes" }, { - "source": 66, - "target": 68, + "source": 65, + "target": 67, "type": "consumes" }, { - "source": 46, - "target": 66, + "source": 45, + "target": 65, "type": "produces" }, { - "source": 68, - "target": 66, + "source": 67, + "target": 65, "type": "produces" }, { "source": 20, - "target": 66, + "target": 65, "type": "produces" }, { - "source": 69, + "source": 68, "target": 2, "type": "consumes" }, { - "source": 17, - "target": 69, + "source": 4, + "target": 68, "type": "produces" }, { "source": 5, - "target": 69, + "target": 68, "type": "produces" }, { - "source": 70, + "source": 69, "target": 7, "type": "consumes" }, { - "source": 48, - "target": 70, + "source": 47, + "target": 69, "type": "produces" }, { - "source": 71, - "target": 48, + "source": 70, + "target": 47, "type": "consumes" }, { - "source": 72, + "source": 71, "target": 2, "type": "consumes" }, { - "source": 72, - "target": 73, + "source": 71, + "target": 72, "type": "consumes" }, { "source": 20, - "target": 72, + "target": 71, "type": "produces" }, - { - "source": 74, - "target": 72, - "type": "produces" - }, - { - "source": 75, - "target": 10, - "type": "consumes" - }, { "source": 73, - "target": 75, + "target": 71, "type": "produces" }, { - "source": 76, + "source": 74, "target": 3, "type": "consumes" }, { "source": 20, - "target": 76, + "target": 74, "type": "produces" }, { - "source": 77, - "target": 78, + "source": 75, + "target": 76, "type": "consumes" }, { "source": 20, - "target": 77, + "target": 75, "type": "produces" }, { - "source": 79, + "source": 77, "target": 2, "type": "consumes" }, { - "source": 79, + "source": 77, "target": 20, "type": "consumes" }, { "source": 10, - "target": 79, + "target": 77, "type": "produces" }, { - "source": 80, + "source": 78, "target": 16, "type": "consumes" }, { - "source": 42, - "target": 80, + "source": 41, + "target": 78, "type": "produces" }, { - "source": 81, + "source": 79, "target": 7, "type": "consumes" }, { "source": 7, - "target": 81, - "type": "produces" - }, - { - "source": 82, - "target": 3, - "type": "consumes" - }, - { - "source": 5, - "target": 82, + "target": 79, "type": "produces" }, { - "source": 83, + "source": 80, "target": 3, "type": "consumes" }, { - "source": 46, - "target": 83, + "source": 45, + "target": 80, "type": "produces" }, { "source": 4, - "target": 83, + "target": 80, "type": "produces" }, { - "source": 84, - "target": 46, + "source": 81, + "target": 45, "type": "consumes" }, { "source": 10, - "target": 84, + "target": 81, "type": "produces" }, { - "source": 85, - "target": 46, + "source": 82, + "target": 45, "type": "consumes" }, { "source": 10, - "target": 85, + "target": 82, "type": "produces" }, { - "source": 86, + "source": 83, "target": 7, "type": "consumes" }, { - "source": 46, - "target": 86, + "source": 45, + "target": 83, "type": "produces" }, { "source": 20, - "target": 86, + "target": 83, "type": "produces" }, { - "source": 87, - "target": 67, + "source": 84, + "target": 66, "type": "consumes" }, { - "source": 87, - "target": 68, + "source": 84, + "target": 67, "type": "consumes" }, { - "source": 46, - "target": 87, + "source": 45, + "target": 84, "type": "produces" }, { - "source": 88, + "source": 85, "target": 7, "type": "consumes" }, { - "source": 48, - "target": 88, + "source": 47, + "target": 85, "type": "produces" }, { - "source": 68, - "target": 88, + "source": 67, + "target": 85, "type": "produces" }, { - "source": 89, - "target": 46, + "source": 86, + "target": 45, "type": "consumes" }, { "source": 10, - "target": 89, + "target": 86, "type": "produces" }, { - "source": 90, - "target": 68, + "source": 87, + "target": 67, "type": "consumes" }, { - "source": 46, - "target": 90, + "source": 45, + "target": 87, "type": "produces" }, { - "source": 91, + "source": 88, "target": 2, "type": "consumes" }, { - "source": 91, - "target": 68, + "source": 88, + "target": 67, "type": "consumes" }, { - "source": 91, - "target": 17, + "source": 88, + "target": 5, "type": "consumes" }, { - "source": 46, - "target": 91, + "source": 45, + "target": 88, "type": "produces" }, { "source": 4, - "target": 91, + "target": 88, "type": "produces" }, { - "source": 68, - "target": 91, + "source": 67, + "target": 88, "type": "produces" }, { - "source": 17, - "target": 91, + "source": 5, + "target": 88, "type": "produces" }, { - "source": 92, - "target": 46, + "source": 89, + "target": 45, "type": "consumes" }, { - "source": 92, - "target": 67, + "source": 89, + "target": 66, "type": "consumes" }, { "source": 9, - "target": 92, + "target": 89, "type": "produces" }, { - "source": 93, - "target": 68, + "source": 90, + "target": 67, "type": "consumes" }, { - "source": 93, + "source": 90, "target": 3, "type": "consumes" }, { - "source": 17, - "target": 93, + "source": 5, + "target": 90, "type": "produces" }, { "source": 3, - "target": 93, + "target": 90, "type": "produces" }, { "source": 20, - "target": 93, + "target": 90, "type": "produces" }, { - "source": 94, - "target": 93, + "source": 91, + "target": 90, "type": "produces" }, { - "source": 95, + "source": 92, "target": 3, "type": "consumes" }, { "source": 4, - "target": 95, + "target": 92, "type": "produces" }, { - "source": 96, + "source": 93, "target": 7, "type": "consumes" }, { "source": 7, - "target": 96, + "target": 93, "type": "produces" }, { - "source": 97, + "source": 94, "target": 2, "type": "consumes" }, { "source": 4, - "target": 97, + "target": 94, "type": "produces" }, { - "source": 98, + "source": 95, "target": 16, "type": "consumes" }, { - "source": 98, + "source": 95, "target": 3, "type": "consumes" }, { - "source": 98, + "source": 95, "target": 20, "type": "consumes" }, { "source": 2, - "target": 98, + "target": 95, "type": "produces" }, { "source": 3, - "target": 98, + "target": 95, "type": "produces" }, { - "source": 99, - "target": 74, + "source": 96, + "target": 73, "type": "consumes" }, { "source": 4, - "target": 99, + "target": 96, "type": "produces" }, { - "source": 100, + "source": 97, "target": 7, "type": "consumes" }, { "source": 7, - "target": 100, + "target": 97, "type": "produces" }, { - "source": 48, - "target": 100, + "source": 47, + "target": 97, "type": "produces" }, { "source": 20, - "target": 100, + "target": 97, "type": "produces" }, { - "source": 101, + "source": 98, "target": 3, "type": "consumes" }, { - "source": 78, - "target": 101, + "source": 76, + "target": 98, "type": "produces" }, { - "source": 102, + "source": 99, "target": 12, "type": "consumes" }, { - "source": 103, - "target": 102, + "source": 100, + "target": 99, "type": "produces" }, { - "source": 104, + "source": 101, "target": 12, "type": "consumes" }, { "source": 12, - "target": 104, + "target": 101, "type": "produces" }, { - "source": 105, + "source": 102, "target": 12, "type": "consumes" }, { - "source": 103, - "target": 105, + "source": 100, + "target": 102, "type": "produces" }, { - "source": 106, + "source": 103, "target": 10, "type": "consumes" }, { "source": 10, - "target": 106, + "target": 103, "type": "produces" }, { - "source": 107, + "source": 104, + "target": 10, + "type": "consumes" + }, + { + "source": 72, + "target": 104, + "type": "produces" + }, + { + "source": 105, "target": 7, "type": "consumes" }, { "source": 7, - "target": 107, + "target": 105, "type": "produces" }, { - "source": 108, - "target": 42, + "source": 106, + "target": 41, "type": "consumes" }, { "source": 4, - "target": 108, + "target": 106, "type": "produces" }, { - "source": 109, + "source": 107, "target": 3, "type": "consumes" }, { - "source": 109, - "target": 74, + "source": 107, + "target": 73, "type": "consumes" }, { "source": 4, - "target": 109, - "type": "produces" - }, - { - "source": 5, - "target": 109, + "target": 107, "type": "produces" }, { - "source": 110, - "target": 42, + "source": 108, + "target": 41, "type": "consumes" }, { - "source": 5, - "target": 110, + "source": 4, + "target": 108, "type": "produces" }, { - "source": 111, + "source": 109, "target": 7, "type": "consumes" }, { "source": 7, - "target": 111, + "target": 109, "type": "produces" }, { - "source": 112, + "source": 110, "target": 2, "type": "consumes" }, { "source": 4, - "target": 112, + "target": 110, "type": "produces" }, { - "source": 113, + "source": 111, "target": 7, "type": "consumes" }, { - "source": 113, + "source": 111, "target": 2, "type": "consumes" }, { - "source": 113, + "source": 111, "target": 12, "type": "consumes" }, { - "source": 113, + "source": 111, "target": 16, "type": "consumes" }, { - "source": 113, - "target": 42, + "source": 111, + "target": 41, "type": "consumes" }, { - "source": 114, + "source": 112, "target": 2, "type": "consumes" }, { - "source": 114, + "source": 112, "target": 3, "type": "consumes" }, { "source": 7, - "target": 114, + "target": 112, "type": "produces" }, { "source": 4, - "target": 114, + "target": 112, "type": "produces" }, { - "source": 115, + "source": 113, "target": 3, "type": "consumes" }, { "source": 4, - "target": 115, - "type": "produces" - }, - { - "source": 17, - "target": 115, + "target": 113, "type": "produces" }, { "source": 5, - "target": 115, + "target": 113, "type": "produces" }, { - "source": 116, + "source": 114, "target": 7, "type": "consumes" }, { - "source": 116, + "source": 114, "target": 20, "type": "consumes" }, { "source": 7, - "target": 116, + "target": 114, "type": "produces" }, { - "source": 117, + "source": 115, "target": 7, "type": "consumes" }, { "source": 7, - "target": 117, + "target": 115, "type": "produces" }, { - "source": 118, + "source": 116, "target": 2, "type": "consumes" }, { - "source": 118, - "target": 74, + "source": 116, + "target": 73, "type": "consumes" }, { - "source": 4, - "target": 118, - "type": "produces" - }, - { - "source": 74, - "target": 118, + "source": 73, + "target": 116, "type": "produces" }, { - "source": 119, + "source": 117, "target": 2, "type": "consumes" }, { - "source": 119, - "target": 74, + "source": 117, + "target": 73, "type": "consumes" }, { - "source": 4, - "target": 119, - "type": "produces" - }, - { - "source": 74, - "target": 119, + "source": 73, + "target": 117, "type": "produces" }, { - "source": 120, + "source": 118, "target": 2, "type": "consumes" }, { - "source": 120, - "target": 74, + "source": 118, + "target": 73, "type": "consumes" }, { - "source": 74, - "target": 120, + "source": 73, + "target": 118, "type": "produces" }, { - "source": 121, + "source": 119, "target": 7, "type": "consumes" }, { "source": 7, - "target": 121, + "target": 119, "type": "produces" }, { - "source": 122, + "source": 120, "target": 7, "type": "consumes" }, { - "source": 48, - "target": 122, + "source": 47, + "target": 120, "type": "produces" }, { - "source": 123, + "source": 121, "target": 16, "type": "consumes" }, { - "source": 123, + "source": 121, "target": 3, "type": "consumes" }, { - "source": 123, + "source": 121, "target": 20, "type": "consumes" }, { - "source": 124, + "source": 122, "target": 7, "type": "consumes" }, { - "source": 124, + "source": 122, "target": 12, "type": "consumes" }, { - "source": 124, - "target": 125, + "source": 122, + "target": 123, "type": "consumes" }, { "source": 16, - "target": 124, + "target": 122, "type": "produces" }, { - "source": 126, + "source": 124, + "target": 66, + "type": "consumes" + }, + { + "source": 124, "target": 67, "type": "consumes" }, + { + "source": 45, + "target": 124, + "type": "produces" + }, + { + "source": 125, + "target": 45, + "type": "consumes" + }, + { + "source": 10, + "target": 125, + "type": "produces" + }, { "source": 126, - "target": 68, + "target": 7, "type": "consumes" }, { - "source": 46, + "source": 7, "target": 126, "type": "produces" }, { "source": 127, - "target": 46, + "target": 73, "type": "consumes" }, { - "source": 10, + "source": 4, "target": 127, "type": "produces" }, { "source": 128, - "target": 7, + "target": 20, "type": "consumes" }, { - "source": 7, + "source": 4, "target": 128, "type": "produces" }, { "source": 129, - "target": 74, + "target": 3, "type": "consumes" }, { - "source": 4, + "source": 20, "target": 129, "type": "produces" }, { "source": 130, - "target": 20, + "target": 7, "type": "consumes" }, { - "source": 4, + "source": 7, "target": 130, "type": "produces" }, { "source": 131, - "target": 3, + "target": 7, "type": "consumes" }, + { + "source": 47, + "target": 131, + "type": "produces" + }, { "source": 20, "target": 131, @@ -1466,442 +1441,422 @@ }, { "source": 133, - "target": 7, + "target": 12, "type": "consumes" }, { - "source": 48, + "source": 4, "target": 133, "type": "produces" }, { - "source": 20, + "source": 16, "target": 133, "type": "produces" }, { - "source": 134, - "target": 7, - "type": "consumes" + "source": 40, + "target": 133, + "type": "produces" }, { - "source": 7, - "target": 134, + "source": 5, + "target": 133, "type": "produces" }, { - "source": 135, + "source": 134, "target": 7, "type": "consumes" }, { - "source": 135, + "source": 134, "target": 12, "type": "consumes" }, { "source": 7, - "target": 135, + "target": 134, "type": "produces" }, { "source": 4, - "target": 135, + "target": 134, "type": "produces" }, { "source": 16, - "target": 135, - "type": "produces" - }, - { - "source": 17, - "target": 135, + "target": 134, "type": "produces" }, { "source": 5, - "target": 135, + "target": 134, "type": "produces" }, { - "source": 136, + "source": 135, "target": 7, "type": "consumes" }, { "source": 7, - "target": 136, + "target": 135, "type": "produces" }, { - "source": 137, + "source": 136, "target": 7, "type": "consumes" }, { - "source": 48, - "target": 137, + "source": 47, + "target": 136, "type": "produces" }, { - "source": 138, + "source": 137, "target": 3, "type": "consumes" }, { "source": 4, - "target": 138, + "target": 137, "type": "produces" }, { - "source": 139, + "source": 138, "target": 20, "type": "consumes" }, { - "source": 68, - "target": 139, + "source": 67, + "target": 138, "type": "produces" }, { - "source": 140, - "target": 141, + "source": 139, + "target": 19, "type": "consumes" }, { - "source": 140, + "source": 139, "target": 7, "type": "consumes" }, { - "source": 140, - "target": 23, + "source": 139, + "target": 22, "type": "consumes" }, { - "source": 140, + "source": 139, "target": 2, "type": "consumes" }, { - "source": 140, + "source": 139, "target": 12, "type": "consumes" }, { - "source": 140, - "target": 125, + "source": 139, + "target": 123, "type": "consumes" }, { - "source": 140, - "target": 68, + "source": 139, + "target": 67, "type": "consumes" }, { - "source": 140, - "target": 25, + "source": 139, + "target": 24, "type": "consumes" }, { - "source": 140, + "source": 139, "target": 3, "type": "consumes" }, { - "source": 140, + "source": 139, "target": 20, "type": "consumes" }, { - "source": 140, - "target": 51, + "source": 139, + "target": 50, "type": "consumes" }, { "source": 7, - "target": 140, + "target": 139, "type": "produces" }, { "source": 4, - "target": 140, + "target": 139, "type": "produces" }, { "source": 12, - "target": 140, + "target": 139, "type": "produces" }, { "source": 16, - "target": 140, + "target": 139, "type": "produces" }, { - "source": 67, - "target": 140, + "source": 66, + "target": 139, "type": "produces" }, { - "source": 142, + "source": 140, "target": 16, "type": "consumes" }, { "source": 7, - "target": 142, + "target": 140, "type": "produces" }, { - "source": 48, - "target": 142, + "source": 47, + "target": 140, "type": "produces" }, { - "source": 143, + "source": 141, "target": 7, "type": "consumes" }, { "source": 7, - "target": 143, + "target": 141, "type": "produces" }, { - "source": 144, + "source": 142, "target": 7, "type": "consumes" }, { "source": 7, - "target": 144, + "target": 142, "type": "produces" }, { - "source": 145, + "source": 143, "target": 7, "type": "consumes" }, { - "source": 145, - "target": 23, + "source": 143, + "target": 22, "type": "consumes" }, { - "source": 146, + "source": 144, "target": 2, "type": "consumes" }, { - "source": 146, + "source": 144, "target": 3, "type": "consumes" }, { "source": 4, - "target": 146, + "target": 144, "type": "produces" }, { - "source": 5, - "target": 146, + "source": 145, + "target": 45, + "type": "consumes" + }, + { + "source": 145, + "target": 5, + "type": "consumes" + }, + { + "source": 145, + "target": 20, + "type": "consumes" + }, + { + "source": 4, + "target": 145, "type": "produces" }, { - "source": 147, + "source": 146, "target": 7, "type": "consumes" }, { "source": 7, - "target": 147, + "target": 146, "type": "produces" }, { - "source": 148, - "target": 46, + "source": 147, + "target": 45, "type": "consumes" }, { - "source": 148, + "source": 147, "target": 10, "type": "consumes" }, { - "source": 148, + "source": 147, "target": 2, "type": "consumes" }, { - "source": 148, - "target": 73, + "source": 147, + "target": 72, "type": "consumes" }, { "source": 4, - "target": 148, - "type": "produces" - }, - { - "source": 5, - "target": 148, + "target": 147, "type": "produces" }, { - "source": 149, + "source": 148, "target": 10, "type": "consumes" }, { "source": 10, - "target": 149, + "target": 148, "type": "produces" }, { - "source": 150, + "source": 149, "target": 3, "type": "consumes" }, { "source": 4, - "target": 150, + "target": 149, "type": "produces" }, { - "source": 151, + "source": 150, "target": 7, "type": "consumes" }, { "source": 7, - "target": 151, + "target": 150, "type": "produces" }, { "source": 20, - "target": 151, - "type": "produces" - }, - { - "source": 152, - "target": 3, - "type": "consumes" - }, - { - "source": 7, - "target": 152, - "type": "produces" - }, - { - "source": 153, - "target": 152, + "target": 150, "type": "produces" }, { - "source": 154, + "source": 151, "target": 7, "type": "consumes" }, { "source": 7, - "target": 154, + "target": 151, "type": "produces" }, { - "source": 155, + "source": 152, "target": 7, "type": "consumes" }, { "source": 7, - "target": 155, + "target": 152, "type": "produces" }, { - "source": 156, + "source": 153, "target": 3, "type": "consumes" }, { - "source": 18, - "target": 156, + "source": 17, + "target": 153, "type": "produces" }, { - "source": 157, + "source": 154, "target": 7, "type": "consumes" }, { "source": 7, - "target": 157, + "target": 154, "type": "produces" }, { "source": 20, - "target": 157, + "target": 154, "type": "produces" }, { - "source": 158, - "target": 74, + "source": 155, + "target": 73, "type": "consumes" }, { - "source": 159, + "source": 156, "target": 4, "type": "consumes" }, { - "source": 159, - "target": 17, + "source": 156, + "target": 5, "type": "consumes" }, { - "source": 159, + "source": 156, "target": 3, "type": "consumes" }, { - "source": 159, - "target": 153, - "type": "consumes" - }, - { - "source": 159, - "target": 5, + "source": 156, + "target": 157, "type": "consumes" }, { - "source": 160, + "source": 158, "target": 2, "type": "consumes" }, { - "source": 160, - "target": 17, + "source": 158, + "target": 5, "type": "consumes" }, { "source": 4, - "target": 160, + "target": 158, "type": "produces" }, { - "source": 17, - "target": 160, + "source": 5, + "target": 158, "type": "produces" }, { "source": 20, - "target": 160, - "type": "produces" - }, - { - "source": 5, - "target": 160, + "target": 158, "type": "produces" } ] \ No newline at end of file diff --git a/docs/dev/dev_environment.md b/docs/dev/dev_environment.md index b73f660574..94e72abd73 100644 --- a/docs/dev/dev_environment.md +++ b/docs/dev/dev_environment.md @@ -2,28 +2,28 @@ The following will show you how to set up a fully functioning python environment for devving on BBOT. -## Installation (Poetry) +## Installation (uv) -[Poetry](https://python-poetry.org/) is the recommended method of installation if you want to dev on BBOT. To set up a dev environment with Poetry, you can follow these steps: +[uv](https://docs.astral.sh/uv/) is the recommended method of installation if you want to dev on BBOT. To set up a dev environment with uv, you can follow these steps: - Fork [BBOT](https://github.com/blacklanternsecurity/bbot) on GitHub -- Clone your fork and set up a development environment with Poetry: +- Clone your fork and set up a development environment with uv: ```bash # clone your forked repo and cd into it git clone git@github.com//bbot.git cd bbot -# install poetry -curl -sSL https://install.python-poetry.org | python3 - +# install uv +curl -LsSf https://astral.sh/uv/install.sh | sh # install pip dependencies -poetry install +uv sync --group dev # install pre-commit hooks, etc. -poetry run pre-commit install +uv run pre-commit install # enter virtual environment -poetry shell +source .venv/bin/activate bbot --help ``` diff --git a/docs/dev/index.md b/docs/dev/index.md index ba1a35d946..699c5799c8 100644 --- a/docs/dev/index.md +++ b/docs/dev/index.md @@ -6,14 +6,14 @@ Documented in this section are commonly-used classes and functions within BBOT, ## Adding BBOT to Your Python Project -If you are using Poetry, you can add BBOT to your python environment like this: +If you are using uv, you can add BBOT to your python environment like this: ```bash # stable -poetry add bbot +uv add bbot # bleeding-edge (dev branch) -poetry add bbot --allow-prereleases +uv add bbot --prerelease=allow ``` ## Running a BBOT Scan from Python @@ -68,7 +68,7 @@ For more details, including which types of targets are valid, see [Targets](../s #### Other Custom Options -In many cases, using a [Preset](../scanning/presets.md) like `subdomain-enum` is sufficient. However, the `Scanner` is flexible and accepts many other arguments that can override the default functionality. You can specify [`flags`](../scanning/index.md#flags-f), [`modules`](../scanning/index.md#modules-m), [`output_modules`](../output.md), a [`whitelist` or `blacklist`](../scanning/index.md#whitelists-and-blacklists), and custom [`config` options](../scanning/configuration.md): +In many cases, using a [Preset](../scanning/presets.md) like `subdomain-enum` is sufficient. However, the `Scanner` is flexible and accepts many other arguments that can override the default functionality. You can specify [`flags`](../scanning/index.md#flags-f), [`modules`](../scanning/index.md#modules-m), [`output_modules`](../output.md), a [target list / `seeds` / `blacklist`](../scanning/index.md#targets-seeds-and-blacklists), and custom [`config` options](../scanning/configuration.md): ```python # create a scan against multiple targets @@ -78,8 +78,8 @@ scan = Scanner( "4.3.2.1", # enable these presets presets=["subdomain-enum"], - # whitelist these hosts - whitelist=["evilcorp.com", "evilcorp.org"], + # explicitly define in-scope targets + target=["evilcorp.com", "evilcorp.org"], # blacklist these hosts blacklist=["prod.evilcorp.com"], # also enable these individual modules diff --git a/docs/dev/module_howto.md b/docs/dev/module_howto.md index 49425bff41..1b67d982fd 100644 --- a/docs/dev/module_howto.md +++ b/docs/dev/module_howto.md @@ -10,7 +10,7 @@ Here we'll go over a basic example of writing a custom BBOT module. - the class must have the same name as your file (case-insensitive) 1. Define in `watched_events` what type of data your module will consume 1. Define in `produced_events` what type of data your module will produce -1. Define (via `flags`) whether your module is `active` or `passive`, and whether it's `safe` or `aggressive` +1. Define (via `flags`) whether your module is `active` or `passive`, and optionally whether it's `loud` or `invasive` 1. **Put your main logic in `.handle_event()`** Here is an example of a simple module that performs whois lookups: @@ -21,7 +21,7 @@ from bbot.modules.base import BaseModule class whois(BaseModule): watched_events = ["DNS_NAME"] # watch for DNS_NAME events produced_events = ["WHOIS"] # we produce WHOIS events - flags = ["passive", "safe"] + flags = ["passive"] meta = {"description": "Query WhoisXMLAPI for WHOIS data"} options = {"api_key": ""} # module config options options_desc = {"api_key": "WhoisXMLAPI Key"} diff --git a/docs/dev/target.md b/docs/dev/target.md index 6740cfb744..b5420801c7 100644 --- a/docs/dev/target.md +++ b/docs/dev/target.md @@ -2,7 +2,7 @@ ::: bbot.scanner.target.ScanSeeds -::: bbot.scanner.target.ScanWhitelist +::: bbot.scanner.target.ScanTarget ::: bbot.scanner.target.ScanBlacklist diff --git a/docs/dev/tests.md b/docs/dev/tests.md index 4381981812..b1407193cb 100644 --- a/docs/dev/tests.md +++ b/docs/dev/tests.md @@ -10,13 +10,13 @@ We have GitHub Actions that automatically run tests whenever you open a Pull Req ```bash # lint with ruff -poetry run ruff check +uv run ruff check # format code with ruff -poetry run ruff format +uv run ruff format # run all tests with pytest (takes roughly 30 minutes) -poetry run pytest +uv run pytest ``` ### Running specific tests @@ -25,18 +25,18 @@ If you only want to run a single test, you can select it with `-k`: ```bash # run only the sslcert test -poetry run pytest -k test_module_sslcert +uv run pytest -k test_module_sslcert ``` You can also filter like this: ```bash # run all the module tests except for sslcert -poetry run pytest -k "test_module_ and not test_module_sslcert" +uv run pytest -k "test_module_ and not test_module_sslcert" ``` If you want to see the output of your module, you can enable `--log-cli-level`: ```bash -poetry run pytest --log-cli-level=DEBUG +uv run pytest --log-cli-level=DEBUG ``` ## Example: Writing a Module Test diff --git a/docs/index.md b/docs/index.md index 7f0b9c6be6..4bedf21728 100644 --- a/docs/index.md +++ b/docs/index.md @@ -92,7 +92,7 @@ bbot -t evilcorp.com -p subdomain-enum -m portscan gowitness -n my_scan -o . ```bash # A basic web scan includes robots.txt, storage buckets, IIS shortnames, and other non-intrusive web modules -bbot -t evilcorp.com -p subdomain-enum web-basic +bbot -t evilcorp.com -p subdomain-enum web ``` **Web spider:** diff --git a/docs/modules/custom_yara_rules.md b/docs/modules/custom_yara_rules.md index 5e40856173..0aeba890c1 100644 --- a/docs/modules/custom_yara_rules.md +++ b/docs/modules/custom_yara_rules.md @@ -135,6 +135,23 @@ rule ContainsTitle } ``` +#### Severity and Confidence +``` +rule ContainsTitle +{ + meta: + description = "Contains an HTML title tag" + severity = "HIGH" + confidence = "CONFIRMED" + $title_value = /(.*)?<\/title>/i + condition: + $title_value +} +``` +Confidence and Severity levels will be assigned to the FINDING event produced if there is a match. + + + When run against the Black Lantern Security homepage with the following BBOT command: ``` diff --git a/docs/modules/lightfuzz.md b/docs/modules/lightfuzz.md index ecaf733a4c..c43a105c7c 100644 --- a/docs/modules/lightfuzz.md +++ b/docs/modules/lightfuzz.md @@ -6,114 +6,304 @@ ### What is Lightfuzz? -Lightfuzz is a lightweight web vulnerability scanner built into BBOT. It is designed to find "low-hanging fruit" type vulnerabilities without much overhead and at massive scale. +Lightfuzz is a lightweight web fuzzer built into BBOT. It is designed to find "low-hanging fruit" type vulnerabilities without much overhead and at massive scale. ### What is Lightfuzz NOT? Lightfuzz is not, does not attempt to be, and will never be, a replacement for a full-blown web application scanner. You should not, for example, be running Lightfuzz as a replacement for Burp Suite scanning. Burp Suite scanner will always find more (even though we can find a few things it can't). -It will also not help you *exploit* vulnerabilities. It's job is to point out vulnerabilities, or likely vulnerabilities, or potential vulnerabilities, and then pass them off to you. A great deal of the overhead with traditional scanners comes in the confirmation phase, or in testing exploitation payloads. +It will also not help you *exploit* what it finds. Its job is to point out likely issues and then pass them off to you. A great deal of the overhead with traditional scanners comes in the confirmation phase, or in testing exploitation payloads. -So for example, Lightfuzz may detect an XSS vulnerability for you. But its NOT going to help you figure out which tag you need to use to get around a security filter, or give you any kind of a final payload. It's simply going to tell you that the contents of a given GET parameter are being reflected and that it was able to render an unmodified HTML tag. The rest is up to you. +So for example, Lightfuzz may detect an XSS vulnerability for you. But it's NOT going to help you figure out which tag you need to use to get around a security filter, or give you any kind of a final payload. It's simply going to tell you that the contents of a given GET parameter are being reflected and that it was able to render an unmodified HTML tag. The rest is up to you. ### False Positives -Significant work has gone into minimizing false positives. However, due to the nature of how Lightfuzz works, they are a reality. Random hiccups in network connectivity can cause them in some cases, odd WAF behavior can account for others. +Significant work has gone into minimizing false positives. However, due to the nature of how Lightfuzz works, they are a reality. Random hiccups in network connectivity can cause them in some cases, odd WAF behavior can account for others. -If you see a false positive that you feel is occuring too often or could easily be prevented, please open a GitHub issue and we will take a look! +If you see a false positive that you feel is occurring too often or could easily be prevented, please open a GitHub issue and we will take a look! -### Deadly module +## Findings, Severity, and Confidence -Lightfuzz currently has the `deadly` flag. This is applied to the most aggressive modules to enforce an additional check, requiring explicit acknowledgement of the risk using the `--allow-deadly` command line flag. +All lightfuzz output is emitted as `FINDING` events. Each finding has two key attributes: **severity** and **confidence**. -## Modules +### Severity -Lightfuzz is divided into numerous "submodules". These would typically be ran all together, but they can be configured to be run individually or in any desired configuration. This would be done with the aide of a `preset`, more on those in a moment. +Severity represents how bad the issue is *if it's real*. Levels, from worst to least: + +| Severity | Meaning | +|---|---| +| **CRITICAL** | Full system compromise likely (e.g., command injection, SSTI) | +| **HIGH** | Significant security impact (e.g., SQL injection, SSRF, path traversal, unsafe deserialization) | +| **MEDIUM** | Moderate impact, often client-side (e.g., XSS, ESI) | +| **LOW** | Minor or limited-scope issue | +| **INFORMATIONAL** | Interesting observation, not directly exploitable (e.g., cryptographic parameter detected) | + +### Confidence + +Confidence represents how sure lightfuzz is that the finding is real, independent of severity. This is critical for triaging results — a HIGH-severity finding with LOW confidence needs manual verification, while a CRITICAL-severity finding with CONFIRMED confidence is almost certainly real. + +| Confidence | Indicator | Meaning | Example | +|---|---|---|---| +| **CONFIRMED** | 🟣 | Out-of-band proof of exploitation | Blind command injection or SSRF via HTTP interaction (Interactsh) | +| **HIGH** | 🔴 | Deterministic detection that's hard to fake | SSTI math evaluation (`1337*1337=1787569`), padding oracle | +| **MEDIUM** | 🟠 | Context-aware detection, but not definitive | SQL error strings after quote injection, XSS tag survival in reflection | +| **LOW** | 🟡 | Heuristic detection, requires manual verification | Blind SQLi timing (even with 3x confirmation), path traversal divergence, deserialization error changes | + +When reviewing lightfuzz output, consider both dimensions together. A `CRITICAL` severity / `CONFIRMED` confidence finding (like OOB command injection) is something to act on immediately. A `HIGH` severity / `LOW` confidence finding (like blind SQLi timing) is worth investigating but may be a false positive. + +## Submodules + +Lightfuzz is divided into numerous "submodules". These would typically be run all together, but they can be configured to be run individually or in any desired configuration. This would be done with the aid of a `preset`, more on those in a moment. ### `cmdi` (Command Injection) - - Finds output-based on blind out-of-band (via `Interactsh`) command injections + - Finds output-based and blind out-of-band (via `Interactsh`) command injections ### `crypto` (Cryptography) - - Identifies cryptographic parameters that have a tangable effect on the application - - Can identify padding oracle vulnerabilities - - Can identify hash length extention vulnerabilities + - Identifies cryptographic parameters that have a tangible effect on the application + - Can identify padding oracle vulnerabilities + - Can identify hash length extension vulnerabilities ### `path` (Path Traversal) - - Can find arbitrary file read / local-file include vulnerabilities, based on relative path traversal or with absolute paths + - Can find arbitrary file read / local-file include vulnerabilities, based on relative path traversal or with absolute paths ### `serial` (Deserialization) - - Can identify the active deserialization of a variety of deserialization types across several platforms + - Can identify the active deserialization of a variety of deserialization types across several platforms ### `sqli` (SQL Injection) - - Error Based SQLi Detection - - Blind time-delay SQLi Detection + - Error Based SQLi Detection + - Blind time-delay SQLi Detection ### `ssti` (Server-side Template Injection) - - Can find basic server-side template injection + - Can find basic server-side template injection ### `xss` (Cross-site Scripting) - - Can find a variety of XSS types, across several different contexts (between-tags, attribute, Javascript-based) -## Presets + - Can find a variety of XSS types, across several different contexts (between-tags, attribute, Javascript-based) +### `esi` (Edge Side Includes) + - Detects Edge Side Include processing +### `ssrf` (Server-Side Request Forgery) + - Detects SSRF vulnerabilities via out-of-band DNS/HTTP interactions using Interactsh -Lightfuzz comes with a few pre-defined presets. The first thing to know is that, unless you really know BBOT inside and out, we recommend using one of them. This is because to be successful, Lightfuzz needs to change a lot of very important BBOT settings. These include: +## Submodule Details -* Setting `url_querystring_remove` to False. By default, BBOT strips away querystings, so in order to FUZZ GET parameters, that default has to be disabled. -``` -url_querystring_remove: False -``` -* Enabling several other complimentary modules. Specifically, `hunt` and `reflected_parameters` can be useful companion modules that also be useful when `WEB_PARAMETER` events are being emitted. +### `cmdi` — Command Injection + +Detects OS command injection via two techniques: + +**Echo Canary Detection** (CRITICAL severity, MEDIUM confidence): Injects command delimiters (`;`, `&&`, `||`, `&`, `|`) combined with an `echo` command containing a random numeric canary. If the canary appears in the response *without* the word "echo" (ruling out simple reflection), injection is indicated. A false-positive probe (`AAAA`) is sent first — if the canary appears with that delimiter, the test aborts since the application is just reflecting input. + +**Blind OOB via Interactsh** (CRITICAL severity, CONFIRMED confidence): Injects `nslookup` commands pointed at unique Interactsh subdomains. If a DNS interaction is received, command execution is confirmed out-of-band. This is the highest-confidence detection lightfuzz can produce. Requires Interactsh to be enabled (it is by default). + +### `sqli` — SQL Injection + +Detects SQL injection via two techniques: + +**Error-based Detection** (HIGH severity, MEDIUM confidence): Injects a single quote (`'`) and compares the response against a list of known SQL error strings (e.g., "error in your SQL syntax", "Unterminated string literal"). Also performs a differential test: if a single quote changes the status code but two single quotes (`''`) produce a *different* status code, it suggests the quotes are being parsed as SQL syntax rather than just rejected. + +**Blind Time-delay Detection** (HIGH severity, LOW confidence): Sends database-specific sleep payloads (PostgreSQL `pg_sleep`, MySQL `SLEEP`, Oracle `DBMS_LOCK.SLEEP`, MSSQL `WAITFOR DELAY`) with a 5-second delay. Measures response time against a baseline average of two normal requests. Requires 3 consecutive confirmations within an acceptable margin (1.5s) to report — even a single miss aborts the test for that payload. Despite the triple confirmation, timing-based detection is inherently loud, hence the LOW confidence. + +### `xss` — Cross-Site Scripting + +Detects reflected XSS across multiple injection contexts. All XSS findings are MEDIUM severity, MEDIUM confidence. + +**Step 1 — Reflection Check**: Sends a random 8-character alphanumeric string. If it doesn't appear in the response, the parameter doesn't reflect and XSS testing is skipped entirely. Parameters from `paramminer_getparams` without an `http-reflection` tag are also skipped. + +**Step 2 — Context Detection**: Determines where in the HTML the reflection occurs: + +- **Between tags**: `<tag>REFLECTION</tag>` +- **In tag attribute**: `<tag attr="REFLECTION">` +- **In JavaScript**: `<script>var x = 'REFLECTION'</script>` + +**Step 3 — Context-Specific Payloads**: + +- *Between tags*: Tests whether HTML tags survive unmodified by injecting `<z>`, `<svg>`, and `<img>` tags. The `z` tag is arbitrary and less likely to be blocked by filters. +- *Tag attribute*: Tests quote escaping (`"z`), auto-quoting behavior, and `javascript:` scheme injection in form actions. +- *JavaScript*: First tries `</script><script>...</script>` tag breaking. If that fails, determines the quote context (single or double) and tests escape-the-escape-character techniques (`\'` → `\\'`). + +Lightfuzz can often detect through WAFs since it does not attempt to construct full exploitation payloads. + +### `path` — Path Traversal + +Detects arbitrary file read / local file inclusion via two techniques: + +**Relative Path Traversal** (HIGH severity, LOW confidence): The core technique is a single-dot vs. double-dot differential. If `./a/../VALUE` returns the same response as the original value (single dot is a no-op) but `../a/../VALUE` returns a *different* response (double dot changes the resolved path), it indicates real path manipulation. This is tested across 8 encoding variants: + +- No encoding (with and without leading slash) +- URL encoding (with and without leading slash) +- Non-recursive stripping bypass (`....//`) +- Double URL encoding +- Start-of-path validation bypass (preserves original directory prefix) + +Each variant requires **4 confirmations** across 5 iterations (one failure tolerated after the first success). WAF responses containing "The requested URL was rejected" are filtered out. Despite the multiple confirmations, this remains LOW confidence because response divergence can have non-traversal explanations. + +**Absolute Path Detection** (HIGH severity, MEDIUM confidence): Directly requests known file paths (`/etc/passwd`, `c:\windows\win.ini`) and checks for expected content strings (`daemon:x:`, `; for 16-bit app support`). Also tests null byte extension bypass (`%00.png`). Higher confidence because matching specific file content is much harder to explain away. + +### `ssti` — Server-Side Template Injection + +Detects template injection via arithmetic evaluation. All SSTI findings are HIGH severity, HIGH confidence. + +Sends the expression `1337*1337` across multiple template syntaxes: + +- `<%= 1337*1337 %>` (ERB/ASP, plus URL-encoded variant) +- `${1337*1337}` (EL/Freemarker, plus URL-encoded variant) +- `1,787{{z}},569` (Jinja2/Twig — if `{{z}}` is stripped, it becomes `1,787,569`) + +If the response contains `1787569` (or `1,787,569`), the expression was evaluated server-side. This is a HIGH confidence detection because it's very unlikely for the exact product of two arbitrary numbers to appear in a response by coincidence. + +### `crypto` — Cryptography Probe + +Identifies cryptographic parameters and probes for cryptographic vulnerabilities. This submodule has several stages with varying confidence levels. +**Stage 1 — Cryptanalysis Gate**: Checks if the parameter value is likely encrypted by calculating Shannon entropy (threshold: 4.5) and whether its decoded length is a multiple of 8 (suggesting a block cipher). If entropy is below threshold, all further tests are skipped. -If you don't want to dive into those details, and we don't blame you, here are the built-in preset options and what you need to know about the differences. +**Stage 2 — Response Divergence** (INFORMATIONAL severity, LOW confidence): Performs byte-level manipulations (truncation and single-byte mutation) and compares responses against both the baseline and an arbitrary garbage value. If the manipulated ciphertext produces a *different* response from both the original *and* garbage input, the parameter likely drives a real cryptographic operation. -# -p lightfuzz-light +**Stage 3 — Error String Detection** (INFORMATIONAL severity, LOW confidence): Scans manipulation responses for cryptographic error messages using YARA rules (e.g., "padding is invalid", "invalid mac", "OpenSSL Error"). Errors present in the baseline are filtered out to avoid false positives. -This is a minimal preset that checks for only the most common vulnerabilities. It enables a select few of lightfuzz's submodules, and is safest for larger scans. +**Stage 4 — Padding Oracle** (HIGH severity, HIGH confidence): If a block cipher is suspected, performs a targeted padding oracle test. Constructs a crafted ciphertext with a null IV block and iterates through all 256 possible last-byte values. A true padding oracle produces a small number of differing responses (1 up to block_size, since multi-byte padding values like `\x02\x02` can also produce valid padding if the intermediate bytes happen to align). To avoid false positives from servers that reflect or reveal submitted/decrypted values, probe values are stripped from both responses before comparison, and small character-level differences (≤5 chars in equal-length responses) are tolerated. Handles the edge case where the baseline byte is the correct padding byte (1/255 chance) by retrying with a different baseline. -# -p lightfuzz-medium +**Stage 5 — Hash Length Extension** (INFORMATIONAL severity, LOW confidence): If the parameter value matches a known hash length (MD5/SHA-1/SHA-256/SHA-384/SHA-512), checks whether modifying *other* parameters on the same request causes the hash parameter's response to change — suggesting those parameters are inputs to the hash, which could enable length extension attacks. -This is the default setting. It enables all lightfuzz submodules, and includes all the necessary config options to make Lightfuzz work, without too many extras. However it is important to note that it **DISABLES FUZZING POST REQUESTS**. This is because this type of request is the most intrusive, and the most likely to cause problems, especially in an internal network. +### `serial` — Unsafe Deserialization -# -p lightfuzz-heavy +Detects active deserialization of serialized objects. All deserialization findings are HIGH severity, LOW confidence. -* Increases the web spider settings a bit from the default. -* Adds in the **Param Miner** suite of modules to try and find new parameters to fuzz via brute-force -* Enables fuzzing of POST parameters +Tests three encoding families (base64, hex, PHP raw) against control baselines crafted to *not* be valid serialized objects. For each family, sends known serialized payloads across multiple platforms: -# -p lightfuzz-superheavy +- **Base64**: PHP, Java (Boolean, String, OptionalDataException), .NET, Ruby +- **Hex**: Java, Java OptionalDataException, .NET +- **PHP Raw**: PHP array -Everything included in `lightfuzz-heavy`, plus: +Two detection methods: -* Query string collapsing turned OFF. Normally, multiple instances of the same parameter (e.g., foo=bar and foo=bar2) are collapsed into one for fuzzing. With `lightfuzz-superheavy`, each instance is fuzzed individually. -* Force common headers enabled - Fuzz certain common header parameters, even if we didn't discover them -* 'Speculate' GET parameters from JSON or XML response bodies +- **Error Resolution**: If the control baseline returns a non-200 status but the serialized payload returns 200 (excluding common error pages like "Internal Server Error"), the application likely recognized and processed the serialized data. +- **Differential Error Analysis**: If the response is a 500 (or a 200 with body changes), checks for deserialization-specific error strings like `java.io.optionaldataexception` or `cannot cast java.lang.string` that weren't present in the baseline. -These settings aren't typically desired as they add significant time to the scan. +Only runs on parameters whose existing values look potentially serialized (base64, hex, or PHP serialization prefixes), or on empty parameters. -# -p lightfuzz-xss +### `esi` — Edge Side Includes -This is a special Lightfuzz preset that focuses entirely on XSS, to make XSS hunting as fast as possible. It is an example of how to make a preset that focuses on specific submodules. It also includes the `paramminer-getparams` module to help find undocumented parameters to fuzz. +Detects ESI processing. ESI findings are MEDIUM severity, HIGH confidence. -# Spider preset +Sends the payload `AA<!--esi-->BB<!--esx-->CC`. If the server processes ESI tags, the `<!--esi-->` comment is removed (it's a valid ESI directive) while `<!--esx-->` is left alone (not a valid directive). The expected detection string is `AABB<!--esx-->CC`. This is a clean differential test — the only explanation for selective comment removal is active ESI processing. -We also *strongly* recommend running Lightfuzz with the spider enabled, as this will dramatically increase the number of parameters that are discovered. If you don't, you will see a warning reminding you that things will work a lot better if you do. +### `ssrf` — Server-Side Request Forgery -That can be done by simply also enabling either the `spider` or `spider-intense` preset. +Detects SSRF vulnerabilities via out-of-band interaction. SSRF findings are HIGH severity, with confidence depending on the interaction type (CONFIRMED for HTTP, MEDIUM for DNS-only). -# Usage +Injects URLs pointing to unique Interactsh subdomains as parameter values. Three URL variants are tested for each parameter: -With the presets in mind, usage is incredibly simple. In most cases you will just do the following: +- `http://TAG.interactsh-domain` — explicit HTTP scheme +- `https://TAG.interactsh-domain` — explicit HTTPS scheme +- `TAG.interactsh-domain` — bare domain (for cases where the server prepends a scheme) + +If the server fetches the injected URL, the resulting DNS or HTTP interaction is detected out-of-band via Interactsh. HTTP interactions are CONFIRMED confidence (the server made a full HTTP request), while DNS-only interactions are MEDIUM confidence (DNS resolution occurred, but the server may not have completed the request). Requires Interactsh to be enabled (it is by default). + +## Presets + +Lightfuzz comes with pre-defined presets. Unless you really know BBOT inside and out, we recommend using one of them. This is because Lightfuzz needs to change several important BBOT settings to work correctly, including: + +* Setting `url_querystring_remove` to False. By default, BBOT strips away querystrings, so in order to fuzz GET parameters, that default has to be disabled. +* Enabling complementary modules. Specifically, `hunt` and `reflected_parameters` can be useful companion modules when `WEB_PARAMETER` events are being emitted. + +If you don't want to dive into those details, here are the built-in preset options. + +### `-p lightfuzz-light` + +The most minimal option. Best for running alongside larger scans with minimal overhead. + +| Setting | Value | +|---|---| +| **Submodules** | `path`, `sqli`, `xss` only | +| **Companion modules** | None | +| **POST fuzzing** | Disabled | +| **WAF avoidance** | On | + +### `-p lightfuzz` + +The default starting point. If you're not sure which to use, start here. + +| Setting | Value | +|---|---| +| **Submodules** | All 9 (`cmdi`, `crypto`, `path`, `serial`, `sqli`, `ssti`, `xss`, `esi`, `ssrf`) | +| **Companion modules** | `badsecrets`, `hunt`, `reflected_parameters` | +| **POST fuzzing** | Disabled, but `try_post_as_get` is on (POST params retested as GET) | +| **WAF avoidance** | On | + +### `-p lightfuzz-heavy` + +Everything in `lightfuzz`, plus: + +| Added setting | Value | +|---|---| +| **Param Miner** | `paramminer_headers`, `paramminer_getparams`, `paramminer_cookies` — brute-force discovery of hidden parameters | +| **POST fuzzing** | Enabled | +| **`try_get_as_post`** | On — GET params are also retested as POST | +| **`robots.txt`** | Parsed for additional URL discovery | + +### `-p lightfuzz-max` + +Everything in `lightfuzz-heavy`, plus: + +| Added setting | Value | +|---|---| +| **WAF avoidance** | Off — WAF-protected targets are fuzzed | +| **Query string collapsing** | Off — each unique parameter-value pair is fuzzed individually instead of deduplicating by parameter name | +| **Force common headers** | On — headers like `X-Forwarded-For` are fuzzed even if not observed on the target | +| **Speculate params** | On — potential parameters are extracted from JSON/XML response bodies | + +These settings significantly increase scan time and traffic. Not recommended for routine scanning. + +### `-p lightfuzz-xss` + +A focused preset for XSS hunting only. Demonstrates how to build a single-submodule preset. + +| Setting | Value | +|---|---| +| **Submodules** | `xss` only | +| **Companion modules** | `paramminer_getparams`, `reflected_parameters` | +| **POST fuzzing** | Disabled | +| **Query string collapsing** | Off | + +### Spider Preset + +We *strongly* recommend running Lightfuzz with the spider enabled, as this will dramatically increase the number of parameters discovered. If you don't, you will see a warning reminding you. + +Enable the spider by adding either the `spider` or `spider-heavy` preset: + +``` +bbot -p lightfuzz spider -t targets.txt +``` + +## Usage + +With presets in mind, usage is simple: ``` -bbot -p lightfuzz-medium spider -t targets.txt --allow-deadly +bbot -p lightfuzz spider -t targets.txt ``` -It's really that simple. Almost all output from Lightfuzz will be in the form of a `FINDING`, as opposed to a `VULNERABILITY`, with a couple of exceptions. This is because, as was explained, the nature of the findings are that they are typically unconfirmed and will require work on your part to do so. +All output from Lightfuzz will be `FINDING` events, each with a severity and confidence level. Focus your triage on confidence first — CONFIRMED and HIGH confidence findings are the most actionable, while LOW confidence findings should be treated as leads requiring manual verification. + +### Selecting Specific Submodules -If you wanted a specific submodule, you could make your own preset adjusting the `modules.lightfuzz.enabled_submodules` setting, or do so via the command line: +If you want a specific submodule, you can make your own preset adjusting the `modules.lightfuzz.enabled_submodules` setting, or do so via the command line: Just XSS: ``` -bbot -p lightfuzz-medium -t targets.txt -c modules.lightfuzz.enabled_submodules=[xss] --allow-deadly +bbot -p lightfuzz -t targets.txt -c modules.lightfuzz.enabled_submodules=[xss] ``` XSS and SQLi: ``` -bbot -p lightfuzz-medium -t targets.txt -c modules.lightfuzz.enabled_submodules=[xss,sqli] --allow-deadly +bbot -p lightfuzz -t targets.txt -c modules.lightfuzz.enabled_submodules=[xss,sqli] ``` +## HTTP Method Switching + +Two options are available to test parameters across HTTP methods: + +* **`try_post_as_get`**: For each `POSTPARAM` discovered, also fuzz it as a `GETPARAM`. This catches cases where a parameter accepted via POST is also accepted via GET, which may bypass server-side protections that only apply to one method. +* **`try_get_as_post`**: For each `GETPARAM` discovered, also fuzz it as a `POSTPARAM`. This catches cases where GET parameters are also accepted in POST bodies, potentially with different security controls. +These options are disabled by default. When enabled, findings from converted parameters will be annotated with their original method (e.g., "converted from POSTPARAM") in the finding description. + +This is useful because web frameworks often accept parameters from multiple sources, but security filters (WAFs, input validation) may only apply to the originally intended method. + +Enable via config: +``` +bbot -p lightfuzz -t targets.txt -c modules.lightfuzz.try_post_as_get=true modules.lightfuzz.try_get_as_post=true +``` diff --git a/docs/modules/list_of_modules.md b/docs/modules/list_of_modules.md index 4205db8810..fad65e1ecd 100644 --- a/docs/modules/list_of_modules.md +++ b/docs/modules/list_of_modules.md @@ -1,153 +1,158 @@ # List of Modules <!-- BBOT MODULES --> -| Module | Type | Needs API Key | Description | Flags | Consumed Events | Produced Events | Author | Created Date | -|-----------------------|----------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|---------------------------|----------------| -| ajaxpro | scan | No | Check for potentially vulnerable Ajaxpro instances | active, safe, web-thorough | HTTP_RESPONSE, URL | FINDING, VULNERABILITY | @liquidsec | 2024-01-18 | -| aspnet_bin_exposure | scan | No | Check for ASP.NET Security Feature Bypasses (CVE-2023-36899 and CVE-2023-36560) | active, safe, web-thorough | URL | VULNERABILITY | @liquidsec | 2025-01-28 | -| baddns | scan | No | Check hosts for domain/subdomain takeovers | active, baddns, cloud-enum, safe, subdomain-hijack, web-basic | DNS_NAME, DNS_NAME_UNRESOLVED | FINDING, VULNERABILITY | @liquidsec | 2024-01-18 | -| baddns_direct | scan | No | Check for unusual subdomain / service takeover edge cases that require direct detection | active, baddns, cloud-enum, safe, subdomain-enum | STORAGE_BUCKET, URL | FINDING, VULNERABILITY | @liquidsec | 2024-01-29 | -| baddns_zone | scan | No | Check hosts for DNS zone transfers and NSEC walks | active, baddns, cloud-enum, safe, subdomain-enum | DNS_NAME | FINDING, VULNERABILITY | @liquidsec | 2024-01-29 | -| badsecrets | scan | No | Library for detecting known or weak secrets across many web frameworks | active, safe, web-basic | HTTP_RESPONSE | FINDING, TECHNOLOGY, VULNERABILITY | @liquidsec | 2022-11-19 | -| bucket_amazon | scan | No | Check for S3 buckets related to target | active, cloud-enum, safe, web-basic | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2022-11-04 | -| bucket_digitalocean | scan | No | Check for DigitalOcean spaces related to target | active, cloud-enum, safe, slow, web-thorough | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2022-11-08 | -| bucket_firebase | scan | No | Check for open Firebase databases related to target | active, cloud-enum, safe, web-basic | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2023-03-20 | -| bucket_google | scan | No | Check for Google object storage related to target | active, cloud-enum, safe, web-basic | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2022-11-04 | -| bucket_microsoft | scan | No | Check for Azure storage blobs related to target | active, cloud-enum, safe, web-basic | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2022-11-04 | -| bypass403 | scan | No | Check 403 pages for common bypasses | active, aggressive, web-thorough | URL | FINDING | @liquidsec | 2022-07-05 | -| dnsbrute | scan | No | Brute-force subdomains with massdns + static wordlist | active, aggressive, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2024-04-24 | -| dnsbrute_mutations | scan | No | Brute-force subdomains with massdns + target-specific mutations | active, aggressive, slow, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2024-04-25 | -| dnscommonsrv | scan | No | Check for common SRV records | active, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-05-15 | -| dotnetnuke | scan | No | Scan for critical DotNetNuke (DNN) vulnerabilities | active, aggressive, web-thorough | HTTP_RESPONSE | TECHNOLOGY, VULNERABILITY | @liquidsec | 2023-11-21 | -| ffuf | scan | No | A fast web fuzzer written in Go | active, aggressive, deadly | URL | URL_UNVERIFIED | @liquidsec | 2022-04-10 | -| ffuf_shortnames | scan | No | Use ffuf in combination IIS shortnames | active, aggressive, iis-shortnames, web-thorough | URL_HINT | URL_UNVERIFIED | @liquidsec | 2022-07-05 | -| filedownload | scan | No | Download common filetypes such as PDF, DOCX, PPTX, etc. | active, download, safe, web-basic | HTTP_RESPONSE, URL_UNVERIFIED | FILESYSTEM | @TheTechromancer | 2023-10-11 | -| fingerprintx | scan | No | Fingerprint exposed services like RDP, SSH, MySQL, etc. | active, safe, service-enum, slow | OPEN_TCP_PORT | PROTOCOL | @TheTechromancer | 2023-01-30 | -| generic_ssrf | scan | No | Check for generic SSRFs | active, aggressive, web-thorough | URL | VULNERABILITY | @liquidsec | 2022-07-30 | -| git | scan | No | Check for exposed .git repositories | active, code-enum, safe, web-basic | URL | CODE_REPOSITORY, FINDING | @TheTechromancer | 2023-05-30 | -| gitlab_com | scan | No | Enumerate GitLab SaaS (gitlab.com/org) for projects and groups | active, code-enum, safe | SOCIAL | CODE_REPOSITORY | @TheTechromancer | 2024-03-11 | -| gitlab_onprem | scan | No | Detect self-hosted GitLab instances and query them for repositories | active, code-enum, safe | HTTP_RESPONSE, SOCIAL, TECHNOLOGY | CODE_REPOSITORY, FINDING, SOCIAL, TECHNOLOGY | @TheTechromancer | 2024-03-11 | -| gowitness | scan | No | Take screenshots of webpages | active, safe, web-screenshots | SOCIAL, URL | TECHNOLOGY, URL, URL_UNVERIFIED, WEBSCREENSHOT | @TheTechromancer | 2022-07-08 | -| graphql_introspection | scan | No | Perform GraphQL introspection on a target | active, safe, web-basic | URL | FINDING | @mukesh-dream11 | 2025-07-01 | -| host_header | scan | No | Try common HTTP Host header spoofing techniques | active, aggressive, web-thorough | HTTP_RESPONSE | FINDING | @liquidsec | 2022-07-27 | -| httpx | scan | No | Visit webpages. Many other modules rely on httpx | active, cloud-enum, safe, social-enum, subdomain-enum, web-basic | OPEN_TCP_PORT, URL, URL_UNVERIFIED | HTTP_RESPONSE, URL | @TheTechromancer | 2022-07-08 | -| hunt | scan | No | Watch for commonly-exploitable HTTP parameters | active, safe, web-thorough | WEB_PARAMETER | FINDING | @liquidsec | 2022-07-20 | -| iis_shortnames | scan | No | Check for IIS shortname vulnerability | active, iis-shortnames, safe, web-basic | URL | URL_HINT | @liquidsec | 2022-04-15 | -| legba | scan | No | Credential bruteforcing supporting various services. | active, aggressive, deadly | PROTOCOL | FINDING | @christianfl, @fuzikowski | 2025-07-18 | -| lightfuzz | scan | No | Find Web Parameters and Lightly Fuzz them using a heuristic based scanner | active, aggressive, deadly, web-thorough | URL, WEB_PARAMETER | FINDING, VULNERABILITY | @liquidsec | 2024-06-28 | -| medusa | scan | No | Medusa SNMP bruteforcing with v1, v2c and R/W check. | active, aggressive, deadly | PROTOCOL | VULNERABILITY | @christianfl | 2025-05-16 | -| newsletters | scan | No | Searches for Newsletter Submission Entry Fields on Websites | active, safe | HTTP_RESPONSE | FINDING | @stryker2k2 | 2024-02-02 | -| ntlm | scan | No | Watch for HTTP endpoints that support NTLM authentication | active, safe, web-basic | HTTP_RESPONSE, URL | DNS_NAME, FINDING | @liquidsec | 2022-07-25 | -| nuclei | scan | No | Fast and customisable vulnerability scanner | active, aggressive, deadly | URL | FINDING, TECHNOLOGY, VULNERABILITY | @TheTechromancer | 2022-03-12 | -| oauth | scan | No | Enumerate OAUTH and OpenID Connect services | active, affiliates, cloud-enum, safe, subdomain-enum, web-basic | DNS_NAME, URL_UNVERIFIED | DNS_NAME | @TheTechromancer | 2023-07-12 | -| paramminer_cookies | scan | No | Smart brute-force to check for common HTTP cookie parameters | active, aggressive, slow, web-paramminer | HTTP_RESPONSE, WEB_PARAMETER | FINDING, WEB_PARAMETER | @liquidsec | 2022-06-27 | -| paramminer_getparams | scan | No | Use smart brute-force to check for common HTTP GET parameters | active, aggressive, slow, web-paramminer | HTTP_RESPONSE, WEB_PARAMETER | FINDING, WEB_PARAMETER | @liquidsec | 2022-06-28 | -| paramminer_headers | scan | No | Use smart brute-force to check for common HTTP header parameters | active, aggressive, slow, web-paramminer | HTTP_RESPONSE, WEB_PARAMETER | WEB_PARAMETER | @liquidsec | 2022-04-15 | -| portscan | scan | No | Port scan with masscan. By default, scans top 100 ports. | active, portscan, safe | DNS_NAME, IP_ADDRESS, IP_RANGE | OPEN_TCP_PORT | @TheTechromancer | 2024-05-15 | -| reflected_parameters | scan | No | Highlight parameters that reflect their contents in response body | active, safe, web-thorough | WEB_PARAMETER | FINDING | @liquidsec | 2024-10-29 | -| retirejs | scan | No | Detect vulnerable/out-of-date JavaScript libraries | active, safe, web-thorough | URL_UNVERIFIED | FINDING | @liquidsec | 2025-08-19 | -| robots | scan | No | Look for and parse robots.txt | active, safe, web-basic | URL | URL_UNVERIFIED | @liquidsec | 2023-02-01 | -| securitytxt | scan | No | Check for security.txt content | active, cloud-enum, safe, subdomain-enum, web-basic | DNS_NAME | EMAIL_ADDRESS, URL_UNVERIFIED | @colin-stubbs | 2024-05-26 | -| smuggler | scan | No | Check for HTTP smuggling | active, aggressive, slow, web-thorough | URL | FINDING | @liquidsec | 2022-07-06 | -| sslcert | scan | No | Visit open ports and retrieve SSL certificates | active, affiliates, email-enum, safe, subdomain-enum, web-basic | OPEN_TCP_PORT | DNS_NAME, EMAIL_ADDRESS | @TheTechromancer | 2022-03-30 | -| telerik | scan | No | Scan for critical Telerik vulnerabilities | active, aggressive, web-thorough | HTTP_RESPONSE, URL | FINDING, VULNERABILITY | @liquidsec | 2022-04-10 | -| url_manipulation | scan | No | Attempt to identify URL parsing/routing based vulnerabilities | active, aggressive, web-thorough | URL | FINDING | @liquidsec | 2022-09-27 | -| vhost | scan | No | Fuzz for virtual hosts | active, aggressive, deadly, slow | URL | DNS_NAME, VHOST | @liquidsec | 2022-05-02 | -| wafw00f | scan | No | Web Application Firewall Fingerprinting Tool | active, aggressive | URL | WAF | @liquidsec | 2023-02-15 | -| wpscan | scan | No | Wordpress security scanner. Highly recommended to use an API key for better results. | active, aggressive | HTTP_RESPONSE, TECHNOLOGY | FINDING, TECHNOLOGY, URL_UNVERIFIED, VULNERABILITY | @domwhewell-sage | 2024-05-29 | -| affiliates | scan | No | Summarize affiliate domains at the end of a scan | affiliates, passive, safe | * | | @TheTechromancer | 2022-07-25 | -| anubisdb | scan | No | Query jldc.me's database for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-10-04 | -| apkpure | scan | No | Download android applications from apkpure.com | code-enum, download, passive, safe | MOBILE_APP | FILESYSTEM | @domwhewell-sage | 2024-10-11 | -| asn | scan | No | Query ripe and bgpview.io for ASNs | passive, safe, subdomain-enum | IP_ADDRESS | ASN | @TheTechromancer | 2022-07-25 | -| azure_realm | scan | No | Retrieves the "AuthURL" from login.microsoftonline.com/getuserrealm | affiliates, cloud-enum, passive, safe, subdomain-enum, web-basic | DNS_NAME | URL_UNVERIFIED | @TheTechromancer | 2023-07-12 | -| azure_tenant | scan | No | Query Azure via azmap.dev for tenant sister domains | affiliates, cloud-enum, passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2024-07-04 | -| bevigil | scan | Yes | Retrieve OSINT data from mobile applications using BeVigil | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, URL_UNVERIFIED | @alt-glitch | 2022-10-26 | -| bucket_file_enum | scan | No | Works in conjunction with the filedownload module to download files from open storage buckets. Currently supported cloud providers: AWS, DigitalOcean | cloud-enum, passive, safe | STORAGE_BUCKET | URL_UNVERIFIED | @TheTechromancer | 2023-11-14 | -| bufferoverrun | scan | Yes | Query BufferOverrun's TLS API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2024-10-23 | -| builtwith | scan | Yes | Query Builtwith.com for subdomains | affiliates, passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-23 | -| c99 | scan | Yes | Query the C99 API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-08 | -| censys_dns | scan | Yes | Query the Censys API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-04 | -| censys_ip | scan | Yes | Query the Censys API for hosts by IP address | passive, safe | IP_ADDRESS | DNS_NAME, IP_ADDRESS, OPEN_TCP_PORT, OPEN_UDP_PORT, PROTOCOL, TECHNOLOGY, URL_UNVERIFIED | @TheTechromancer | 2026-01-26 | -| certspotter | scan | No | Query Certspotter's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-28 | -| chaos | scan | Yes | Query ProjectDiscovery's Chaos API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-14 | -| code_repository | scan | No | Look for code repository links in webpages | code-enum, passive, safe | URL_UNVERIFIED | CODE_REPOSITORY | @domwhewell-sage | 2024-05-15 | -| credshed | scan | Yes | Send queries to your own credshed server to check for known credentials of your targets | passive, safe | DNS_NAME | EMAIL_ADDRESS, HASHED_PASSWORD, PASSWORD, USERNAME | @SpamFaux | 2023-10-12 | -| crt | scan | No | Query crt.sh (certificate transparency) for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-05-13 | -| crt_db | scan | No | Query crt.sh (certificate transparency) for subdomains via PostgreSQL | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2025-03-27 | -| dehashed | scan | Yes | Execute queries against dehashed.com for exposed credentials | email-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS, HASHED_PASSWORD, PASSWORD, USERNAME | @SpamFaux | 2023-10-12 | -| digitorus | scan | No | Query certificatedetails.com for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2023-07-25 | -| dnsbimi | scan | No | Check DNS_NAME's for BIMI records to find image and certificate hosting URL's | cloud-enum, passive, safe, subdomain-enum | DNS_NAME | RAW_DNS_RECORD, URL_UNVERIFIED | @colin-stubbs | 2024-11-15 | -| dnscaa | scan | No | Check for CAA records | email-enum, passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, EMAIL_ADDRESS, URL_UNVERIFIED | @colin-stubbs | 2024-05-26 | -| dnsdumpster | scan | No | Query dnsdumpster for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-03-12 | -| dnstlsrpt | scan | No | Check for TLS-RPT records | cloud-enum, email-enum, passive, safe, subdomain-enum | DNS_NAME | EMAIL_ADDRESS, RAW_DNS_RECORD, URL_UNVERIFIED | @colin-stubbs | 2024-07-26 | -| docker_pull | scan | No | Download images from a docker repository | code-enum, download, passive, safe, slow | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2024-03-24 | -| dockerhub | scan | No | Search for docker repositories of discovered orgs/usernames | code-enum, passive, safe | ORG_STUB, SOCIAL | CODE_REPOSITORY, SOCIAL, URL_UNVERIFIED | @domwhewell-sage | 2024-03-12 | -| emailformat | scan | No | Query email-format.com for email addresses | email-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS | @TheTechromancer | 2022-07-11 | -| extractous | scan | No | Module to extract data from files | passive, safe | FILESYSTEM | RAW_TEXT | @domwhewell-sage | 2024-06-03 | -| fullhunt | scan | Yes | Query the fullhunt.io API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-24 | -| git_clone | scan | No | Clone code github repositories | code-enum, download, passive, safe, slow | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2024-03-08 | -| gitdumper | scan | No | Download a leaked .git folder recursively or by fuzzing common names | code-enum, download, passive, safe, slow | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2025-02-11 | -| github_codesearch | scan | Yes | Query Github's API for code containing the target domain name | code-enum, passive, safe, subdomain-enum | DNS_NAME | CODE_REPOSITORY, URL_UNVERIFIED | @domwhewell-sage | 2023-12-14 | -| github_org | scan | No | Query Github's API for organization and member repositories | code-enum, passive, safe, subdomain-enum | ORG_STUB, SOCIAL | CODE_REPOSITORY | @domwhewell-sage | 2023-12-14 | -| github_usersearch | scan | Yes | Query Github's API for users with emails matching in scope domains that may not be discoverable by listing members of the organization. | code-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS, SOCIAL | @domwhewell-sage | 2025-05-10 | -| github_workflows | scan | Yes | Download a github repositories workflow logs and workflow artifacts | code-enum, download, passive, safe | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2024-04-29 | -| google_playstore | scan | No | Search for android applications on play.google.com | code-enum, passive, safe | CODE_REPOSITORY, ORG_STUB | MOBILE_APP | @domwhewell-sage | 2024-10-08 | -| hackertarget | scan | No | Query the hackertarget.com API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-28 | -| hunterio | scan | Yes | Query hunter.io for emails | email-enum, passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, EMAIL_ADDRESS, URL_UNVERIFIED | @TheTechromancer | 2022-04-25 | -| ip2location | scan | Yes | Query IP2location.io's API for geolocation information. | passive, safe | IP_ADDRESS | GEOLOCATION | @TheTechromancer | 2023-09-12 | -| ipneighbor | scan | No | Look beside IPs in their surrounding subnet | aggressive, passive, subdomain-enum | IP_ADDRESS | IP_ADDRESS | @TheTechromancer | 2022-06-08 | -| ipstack | scan | Yes | Query IPStack's GeoIP API | passive, safe | IP_ADDRESS | GEOLOCATION | @tycoonslive | 2022-11-26 | -| jadx | scan | No | Decompile APKs and XAPKs using JADX | code-enum, passive, safe | FILESYSTEM | FILESYSTEM | @domwhewell-sage | 2024-11-04 | -| leakix | scan | No | Query leakix.net for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-11 | -| myssl | scan | No | Query myssl.com's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2023-07-10 | -| otx | scan | Yes | Query otx.alienvault.com for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-24 | -| passivetotal | scan | Yes | Query the PassiveTotal API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-08 | -| pgp | scan | No | Query common PGP servers for email addresses | email-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS | @TheTechromancer | 2022-08-10 | -| portfilter | scan | No | Filter out unwanted open ports from cloud/CDN targets | passive, safe | OPEN_TCP_PORT, URL, URL_UNVERIFIED | | @TheTechromancer | 2025-01-06 | -| postman | scan | No | Query Postman's API for related workspaces, collections, requests and download them | code-enum, passive, safe, subdomain-enum | ORG_STUB, SOCIAL | CODE_REPOSITORY | @domwhewell-sage | 2024-09-07 | -| postman_download | scan | No | Download workspaces, collections, requests from Postman | code-enum, download, passive, safe, subdomain-enum | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2024-09-07 | -| rapiddns | scan | No | Query rapiddns.io for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-24 | -| securitytrails | scan | Yes | Query the SecurityTrails API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-03 | -| shodan_dns | scan | Yes | Query Shodan for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-03 | -| shodan_idb | scan | No | Query Shodan's InternetDB for open ports, hostnames, technologies, and vulnerabilities | passive, portscan, safe, subdomain-enum | DNS_NAME, IP_ADDRESS | DNS_NAME, FINDING, OPEN_TCP_PORT, TECHNOLOGY, VULNERABILITY | @TheTechromancer | 2023-12-22 | -| sitedossier | scan | No | Query sitedossier.com for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2023-08-04 | -| skymem | scan | No | Query skymem.info for email addresses | email-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS | @TheTechromancer | 2022-07-11 | -| social | scan | No | Look for social media links in webpages | passive, safe, social-enum | URL_UNVERIFIED | SOCIAL | @TheTechromancer | 2023-03-28 | -| subdomaincenter | scan | No | Query subdomain.center's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2023-07-26 | -| subdomainradar | scan | Yes | Query the Subdomain API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-08 | -| trickest | scan | Yes | Query Trickest's API for subdomains | affiliates, passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @amiremami | 2024-07-27 | -| trufflehog | scan | No | TruffleHog is a tool for finding credentials | code-enum, passive, safe | CODE_REPOSITORY, FILESYSTEM, HTTP_RESPONSE, RAW_TEXT | FINDING, VULNERABILITY | @domwhewell-sage | 2024-03-12 | -| urlscan | scan | No | Query urlscan.io for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, URL_UNVERIFIED | @TheTechromancer | 2022-06-09 | -| viewdns | scan | No | Query viewdns.info's reverse whois for related domains | affiliates, passive, safe | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-04 | -| virustotal | scan | Yes | Query VirusTotal's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-25 | -| wayback | scan | No | Query archive.org's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, URL_UNVERIFIED | @liquidsec | 2022-04-01 | -| asset_inventory | output | No | Merge hosts, open ports, technologies, findings, etc. into a single asset inventory CSV | | DNS_NAME, FINDING, HTTP_RESPONSE, IP_ADDRESS, OPEN_TCP_PORT, TECHNOLOGY, URL, VULNERABILITY, WAF | IP_ADDRESS, OPEN_TCP_PORT | @liquidsec | 2022-09-30 | -| csv | output | No | Output to CSV | | * | | @TheTechromancer | 2022-04-07 | -| discord | output | No | Message a Discord channel when certain events are encountered | | * | | @TheTechromancer | 2023-08-14 | -| emails | output | No | Output any email addresses found belonging to the target domain | email-enum | EMAIL_ADDRESS | | @domwhewell-sage | 2023-12-23 | -| http | output | No | Send every event to a custom URL via a web request | | * | | @TheTechromancer | 2022-04-13 | -| json | output | No | Output to Newline-Delimited JSON (NDJSON) | | * | | @TheTechromancer | 2022-04-07 | -| mysql | output | No | Output scan data to a MySQL database | | * | | @TheTechromancer | 2024-11-13 | -| neo4j | output | No | Output to Neo4j | | * | | @TheTechromancer | 2022-04-07 | -| nmap_xml | output | No | Output to Nmap XML | | DNS_NAME, HTTP_RESPONSE, IP_ADDRESS, OPEN_TCP_PORT, PROTOCOL | | @TheTechromancer | 2024-11-16 | -| postgres | output | No | Output scan data to a SQLite database | | * | | @TheTechromancer | 2024-11-08 | -| python | output | No | Output via Python API | | * | | @TheTechromancer | 2022-09-13 | -| slack | output | No | Message a Slack channel when certain events are encountered | | * | | @TheTechromancer | 2023-08-14 | -| splunk | output | No | Send every event to a splunk instance through HTTP Event Collector | | * | | @w0Tx | 2024-02-17 | -| sqlite | output | No | Output scan data to a SQLite database | | * | | @TheTechromancer | 2024-11-07 | -| stdout | output | No | Output to text | | * | | @TheTechromancer | 2024-04-03 | -| subdomains | output | No | Output only resolved, in-scope subdomains | subdomain-enum | DNS_NAME, DNS_NAME_UNRESOLVED | | @TheTechromancer | 2023-07-31 | -| teams | output | No | Message a Teams channel when certain events are encountered | | * | | @TheTechromancer | 2023-08-14 | -| txt | output | No | Output to text | | * | | @TheTechromancer | 2024-04-03 | -| web_parameters | output | No | Output WEB_PARAMETER names to a file | | WEB_PARAMETER | | @liquidsec | 2025-01-25 | -| web_report | output | No | Create a markdown report with web assets | | FINDING, TECHNOLOGY, URL, VHOST, VULNERABILITY | | @liquidsec | 2023-02-08 | -| websocket | output | No | Output to websockets | | * | | @TheTechromancer | 2022-04-15 | -| cloudcheck | internal | No | Tag events by cloud provider, identify cloud resources like storage buckets | | * | | @TheTechromancer | 2024-07-07 | -| dnsresolve | internal | No | Perform DNS resolution | | * | DNS_NAME, IP_ADDRESS, RAW_DNS_RECORD | @TheTechromancer | 2022-04-08 | -| aggregate | internal | No | Summarize statistics at the end of a scan | passive, safe | | | @TheTechromancer | 2022-07-25 | -| excavate | internal | No | Passively extract juicy tidbits from scan data | passive | HTTP_RESPONSE, RAW_TEXT | URL_UNVERIFIED, WEB_PARAMETER | @liquidsec | 2022-06-27 | -| speculate | internal | No | Derive certain event types from others by common sense | passive | AZURE_TENANT, DNS_NAME, DNS_NAME_UNRESOLVED, HTTP_RESPONSE, IP_ADDRESS, IP_RANGE, SOCIAL, STORAGE_BUCKET, URL, URL_UNVERIFIED, USERNAME | DNS_NAME, FINDING, IP_ADDRESS, OPEN_TCP_PORT, ORG_STUB | @liquidsec | 2022-05-03 | -| unarchive | internal | No | Extract different types of files into folders on the filesystem | passive, safe | FILESYSTEM | FILESYSTEM | @domwhewell-sage | 2024-12-08 | +| Module | Type | Needs API Key | Description | Flags | Consumed Events | Produced Events | Author | Created Date | +|-----------------------|----------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|---------------------------|----------------| +| ajaxpro | scan | No | Check for potentially vulnerable Ajaxpro instances | active, safe, web-heavy | HTTP_RESPONSE, URL | FINDING, TECHNOLOGY | @liquidsec | 2024-01-18 | +| aspnet_bin_exposure | scan | No | Check for ASP.NET Security Feature Bypasses (CVE-2023-36899 and CVE-2023-36560) | active, safe, web-heavy | URL | FINDING | @liquidsec | 2025-01-28 | +| baddns | scan | No | Check hosts for domain/subdomain takeovers | active, baddns, cloud-enum, safe, subdomain-hijack, web | DNS_NAME, DNS_NAME_UNRESOLVED | FINDING | @liquidsec | 2024-01-18 | +| baddns_direct | scan | No | Check for unusual subdomain / service takeover edge cases that require direct detection | active, baddns, cloud-enum, safe, subdomain-enum | STORAGE_BUCKET, URL | FINDING | @liquidsec | 2024-01-29 | +| baddns_zone | scan | No | Check hosts for DNS zone transfers and NSEC walks | active, baddns, cloud-enum, safe, subdomain-enum | DNS_NAME | FINDING | @liquidsec | 2024-01-29 | +| badsecrets | scan | No | Library for detecting known or weak secrets across many web frameworks | active, safe, web | HTTP_RESPONSE | FINDING, TECHNOLOGY | @liquidsec | 2022-11-19 | +| bucket_amazon | scan | No | Check for S3 buckets related to target | active, cloud-enum, safe, web | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2022-11-04 | +| bucket_digitalocean | scan | No | Check for DigitalOcean spaces related to target | active, cloud-enum, safe, slow, web-heavy | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2022-11-08 | +| bucket_firebase | scan | No | Check for open Firebase databases related to target | active, cloud-enum, safe, web | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2023-03-20 | +| bucket_google | scan | No | Check for Google object storage related to target | active, cloud-enum, safe, web | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2022-11-04 | +| bucket_microsoft | scan | No | Check for Azure storage blobs related to target | active, cloud-enum, safe, web | DNS_NAME, STORAGE_BUCKET | FINDING, STORAGE_BUCKET | @TheTechromancer | 2022-11-04 | +| bypass403 | scan | No | Check 403 pages for common bypasses | active, loud, web-heavy | URL | FINDING | @liquidsec | 2022-07-05 | +| dnsbrute | scan | No | Brute-force subdomains with massdns + static wordlist | active, loud, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2024-04-24 | +| dnsbrute_mutations | scan | No | Brute-force subdomains with massdns + target-specific mutations | active, loud, slow, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2024-04-25 | +| dnscommonsrv | scan | No | Check for common SRV records | active, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-05-15 | +| dotnetnuke | scan | No | Scan for critical DotNetNuke (DNN) vulnerabilities | active, invasive, loud, web-heavy | HTTP_RESPONSE | FINDING, TECHNOLOGY | @liquidsec | 2023-11-21 | +| ffuf | scan | No | A fast web fuzzer written in Go | active, loud | URL | URL_UNVERIFIED | @liquidsec | 2022-04-10 | +| ffuf_shortnames | scan | No | Use ffuf in combination IIS shortnames | active, iis-shortnames, loud, web-heavy | URL_HINT | URL_UNVERIFIED | @liquidsec | 2022-07-05 | +| filedownload | scan | No | Download common filetypes such as PDF, DOCX, PPTX, etc. | active, download, safe, web | HTTP_RESPONSE, URL_UNVERIFIED | FILESYSTEM | @TheTechromancer | 2023-10-11 | +| fingerprintx | scan | No | Fingerprint exposed services like RDP, SSH, MySQL, etc. | active, safe, service-enum, slow | OPEN_TCP_PORT | PROTOCOL | @TheTechromancer | 2023-01-30 | +| git | scan | No | Check for exposed .git repositories | active, code-enum, safe, web | URL | CODE_REPOSITORY, FINDING | @TheTechromancer | 2023-05-30 | +| gitlab_com | scan | No | Enumerate GitLab SaaS (gitlab.com/org) for projects and groups | active, code-enum, safe | SOCIAL | CODE_REPOSITORY | @TheTechromancer | 2024-03-11 | +| gitlab_onprem | scan | No | Detect self-hosted GitLab instances and query them for repositories | active, code-enum, safe | HTTP_RESPONSE, SOCIAL, TECHNOLOGY | CODE_REPOSITORY, FINDING, SOCIAL, TECHNOLOGY | @TheTechromancer | 2024-03-11 | +| gowitness | scan | No | Take screenshots of webpages | active, safe, web-screenshots | SOCIAL, URL | TECHNOLOGY, URL, URL_UNVERIFIED, WEBSCREENSHOT | @TheTechromancer | 2022-07-08 | +| graphql_introspection | scan | No | Perform GraphQL introspection on a target | active, safe, web | URL | FINDING | @mukesh-dream11 | 2025-07-01 | +| host_header | scan | No | Try common HTTP Host header spoofing techniques | active, loud, web-heavy | HTTP_RESPONSE | FINDING | @liquidsec | 2022-07-27 | +| httpx | scan | No | Visit webpages. Many other modules rely on httpx | active, cloud-enum, safe, social-enum, subdomain-enum, web | OPEN_TCP_PORT, URL, URL_UNVERIFIED | HTTP_RESPONSE, URL | @TheTechromancer | 2022-07-08 | +| hunt | scan | No | Watch for commonly-exploitable HTTP parameters | active, safe, web-heavy | WEB_PARAMETER | FINDING | @liquidsec | 2022-07-20 | +| iis_shortnames | scan | No | Check for IIS shortname vulnerability | active, iis-shortnames, loud, web | URL | URL_HINT | @liquidsec | 2022-04-15 | +| legba | scan | No | Credential bruteforcing supporting various services. | active, invasive, loud | PROTOCOL | FINDING | @christianfl, @fuzikowski | 2025-07-18 | +| lightfuzz | scan | No | Find Web Parameters and Lightly Fuzz them using a heuristic based scanner | active, invasive, loud, web-heavy | URL, WEB_PARAMETER | FINDING | @liquidsec | 2024-06-28 | +| medusa | scan | No | Medusa SNMP bruteforcing with v1, v2c and R/W check. | active, invasive, loud | PROTOCOL | FINDING | @christianfl | 2025-05-16 | +| newsletters | scan | No | Searches for Newsletter Submission Entry Fields on Websites | active, safe | HTTP_RESPONSE | FINDING | @stryker2k2 | 2024-02-02 | +| ntlm | scan | No | Watch for HTTP endpoints that support NTLM authentication | active, safe, web | HTTP_RESPONSE, URL | DNS_NAME, FINDING | @liquidsec | 2022-07-25 | +| nuclei | scan | No | Fast and customisable vulnerability scanner | active, invasive, loud | URL | FINDING, TECHNOLOGY | @TheTechromancer | 2022-03-12 | +| oauth | scan | No | Enumerate OAUTH and OpenID Connect services | active, affiliates, cloud-enum, safe, subdomain-enum, web | DNS_NAME, URL_UNVERIFIED | DNS_NAME | @TheTechromancer | 2023-07-12 | +| paramminer_cookies | scan | No | Smart brute-force to check for common HTTP cookie parameters | active, loud, slow, web-paramminer | HTTP_RESPONSE, WEB_PARAMETER | WEB_PARAMETER | @liquidsec | 2022-06-27 | +| paramminer_getparams | scan | No | Use smart brute-force to check for common HTTP GET parameters | active, loud, slow, web-paramminer | HTTP_RESPONSE, WEB_PARAMETER | WEB_PARAMETER | @liquidsec | 2022-06-28 | +| paramminer_headers | scan | No | Use smart brute-force to check for common HTTP header parameters | active, loud, slow, web-paramminer | HTTP_RESPONSE, WEB_PARAMETER | WEB_PARAMETER | @liquidsec | 2022-04-15 | +| portscan | scan | No | Port scan with masscan. By default, scans top 100 ports. | active, loud, portscan | DNS_NAME, IP_ADDRESS, IP_RANGE | OPEN_TCP_PORT | @TheTechromancer | 2024-05-15 | +| reflected_parameters | scan | No | Highlight parameters that reflect their contents in response body | active, safe, web-heavy | WEB_PARAMETER | FINDING | @liquidsec | 2024-10-29 | +| retirejs | scan | No | Detect vulnerable/out-of-date JavaScript libraries | active, safe, web-heavy | URL_UNVERIFIED | FINDING | @liquidsec | 2025-08-19 | +| robots | scan | No | Look for and parse robots.txt | active, safe, web | URL | URL_UNVERIFIED | @liquidsec | 2023-02-01 | +| securitytxt | scan | No | Check for security.txt content | active, cloud-enum, safe, subdomain-enum, web | DNS_NAME | EMAIL_ADDRESS, URL_UNVERIFIED | @colin-stubbs | 2024-05-26 | +| smuggler | scan | No | Check for HTTP smuggling | active, invasive, loud, slow, web-heavy | URL | FINDING | @liquidsec | 2022-07-06 | +| sslcert | scan | No | Visit open ports and retrieve SSL certificates | active, affiliates, email-enum, safe, subdomain-enum, web | OPEN_TCP_PORT | DNS_NAME, EMAIL_ADDRESS | @TheTechromancer | 2022-03-30 | +| telerik | scan | No | Scan for critical Telerik vulnerabilities | active, invasive, loud, web-heavy | HTTP_RESPONSE, URL | FINDING | @liquidsec | 2022-04-10 | +| url_manipulation | scan | No | Attempt to identify URL parsing/routing based vulnerabilities | active, loud, web-heavy | URL | FINDING | @liquidsec | 2022-09-27 | +| wafw00f | scan | No | Web Application Firewall Fingerprinting Tool | active, loud | URL | WAF | @liquidsec | 2023-02-15 | +| wpscan | scan | No | Wordpress security scanner. Highly recommended to use an API key for better results. | active, loud | HTTP_RESPONSE, TECHNOLOGY | FINDING, TECHNOLOGY, URL_UNVERIFIED | @domwhewell-sage | 2024-05-29 | +| affiliates | scan | No | Summarize affiliate domains at the end of a scan | affiliates, passive, safe | * | | @TheTechromancer | 2022-07-25 | +| anubisdb | scan | No | Query jldc.me's database for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-10-04 | +| apkpure | scan | No | Download android applications from apkpure.com | code-enum, download, passive, safe | MOBILE_APP | FILESYSTEM | @domwhewell-sage | 2024-10-11 | +| asn | scan | No | Query asndb for ASN information | passive, safe, subdomain-enum | IP_ADDRESS | ASN | @TheTechromancer | 2022-07-25 | +| azure_tenant | scan | No | Query Azure for tenant information using multiple enumeration methods | affiliates, cloud-enum, passive, safe, subdomain-enum | DNS_NAME | AZURE_TENANT, DNS_NAME, FINDING, URL_UNVERIFIED | @TheTechromancer | 2024-07-04 | +| bevigil | scan | Yes | Retrieve OSINT data from mobile applications using BeVigil | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, URL_UNVERIFIED | @alt-glitch | 2022-10-26 | +| bucket_file_enum | scan | No | Works in conjunction with the filedownload module to download files from open storage buckets. Currently supported cloud providers: AWS, DigitalOcean | cloud-enum, passive, safe | STORAGE_BUCKET | URL_UNVERIFIED | @TheTechromancer | 2023-11-14 | +| bufferoverrun | scan | Yes | Query BufferOverrun's TLS API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2024-10-23 | +| builtwith | scan | Yes | Query Builtwith.com for subdomains | affiliates, passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-23 | +| c99 | scan | Yes | Query the C99 API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-08 | +| censys_dns | scan | Yes | Query the Censys API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-04 | +| censys_ip | scan | Yes | Query the Censys API for hosts by IP address | passive, safe | IP_ADDRESS | DNS_NAME, IP_ADDRESS, OPEN_TCP_PORT, OPEN_UDP_PORT, PROTOCOL, TECHNOLOGY, URL_UNVERIFIED | @TheTechromancer | 2026-01-26 | +| certspotter | scan | No | Query Certspotter's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-28 | +| chaos | scan | Yes | Query ProjectDiscovery's Chaos API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-14 | +| code_repository | scan | No | Look for code repository links in webpages | code-enum, passive, safe | URL_UNVERIFIED | CODE_REPOSITORY | @domwhewell-sage | 2024-05-15 | +| credshed | scan | Yes | Send queries to your own credshed server to check for known credentials of your targets | passive, safe | DNS_NAME | EMAIL_ADDRESS, HASHED_PASSWORD, PASSWORD, USERNAME | @SpamFaux | 2023-10-12 | +| crt | scan | No | Query crt.sh (certificate transparency) for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-05-13 | +| crt_db | scan | No | Query crt.sh (certificate transparency) for subdomains via PostgreSQL | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2025-03-27 | +| dehashed | scan | Yes | Execute queries against dehashed.com for exposed credentials | email-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS, HASHED_PASSWORD, PASSWORD, USERNAME | @SpamFaux | 2023-10-12 | +| digitorus | scan | No | Query certificatedetails.com for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2023-07-25 | +| dnsbimi | scan | No | Check DNS_NAME's for BIMI records to find image and certificate hosting URL's | cloud-enum, passive, safe, subdomain-enum | DNS_NAME | RAW_DNS_RECORD, URL_UNVERIFIED | @colin-stubbs | 2024-11-15 | +| dnscaa | scan | No | Check for CAA records | email-enum, passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, EMAIL_ADDRESS, URL_UNVERIFIED | @colin-stubbs | 2024-05-26 | +| dnsdumpster | scan | No | Query dnsdumpster for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-03-12 | +| dnstlsrpt | scan | No | Check for TLS-RPT records | cloud-enum, email-enum, passive, safe, subdomain-enum | DNS_NAME | EMAIL_ADDRESS, RAW_DNS_RECORD, URL_UNVERIFIED | @colin-stubbs | 2024-07-26 | +| docker_pull | scan | No | Download images from a docker repository | code-enum, download, passive, safe, slow | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2024-03-24 | +| dockerhub | scan | No | Search for docker repositories of discovered orgs/usernames | code-enum, passive, safe | ORG_STUB, SOCIAL | CODE_REPOSITORY, SOCIAL, URL_UNVERIFIED | @domwhewell-sage | 2024-03-12 | +| emailformat | scan | No | Query email-format.com for email addresses | email-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS | @TheTechromancer | 2022-07-11 | +| fullhunt | scan | Yes | Query the fullhunt.io API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-24 | +| git_clone | scan | No | Clone code github repositories | code-enum, download, passive, safe, slow | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2024-03-08 | +| gitdumper | scan | No | Download a leaked .git folder recursively or by fuzzing common names | code-enum, download, passive, safe, slow | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2025-02-11 | +| github_codesearch | scan | Yes | Query Github's API for code containing the target domain name | code-enum, passive, safe, subdomain-enum | DNS_NAME | CODE_REPOSITORY, URL_UNVERIFIED | @domwhewell-sage | 2023-12-14 | +| github_org | scan | No | Query Github's API for organization and member repositories | code-enum, passive, safe, subdomain-enum | ORG_STUB, SOCIAL | CODE_REPOSITORY | @domwhewell-sage | 2023-12-14 | +| github_usersearch | scan | Yes | Query Github's API for users with emails matching in scope domains that may not be discoverable by listing members of the organization. | code-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS, SOCIAL | @domwhewell-sage | 2025-05-10 | +| github_workflows | scan | Yes | Download a github repositories workflow logs and workflow artifacts | code-enum, download, passive, safe | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2024-04-29 | +| google_playstore | scan | No | Search for android applications on play.google.com | code-enum, passive, safe | CODE_REPOSITORY, ORG_STUB | MOBILE_APP | @domwhewell-sage | 2024-10-08 | +| hackertarget | scan | No | Query the hackertarget.com API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-28 | +| hunterio | scan | Yes | Query hunter.io for emails | email-enum, passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, EMAIL_ADDRESS, URL_UNVERIFIED | @TheTechromancer | 2022-04-25 | +| ip2location | scan | Yes | Query IP2location.io's API for geolocation information. | passive, safe | IP_ADDRESS | GEOLOCATION | @TheTechromancer | 2023-09-12 | +| ipneighbor | scan | No | Look beside IPs in their surrounding subnet | loud, passive, subdomain-enum | IP_ADDRESS | IP_ADDRESS | @TheTechromancer | 2022-06-08 | +| ipstack | scan | Yes | Query IPStack's GeoIP API | passive, safe | IP_ADDRESS | GEOLOCATION | @tycoonslive | 2022-11-26 | +| jadx | scan | No | Decompile APKs and XAPKs using JADX | code-enum, passive, safe | FILESYSTEM | FILESYSTEM | @domwhewell-sage | 2024-11-04 | +| kreuzberg | scan | No | Module to extract data from files | passive, safe | FILESYSTEM | RAW_TEXT | @domwhewell-sage | 2024-06-03 | +| leakix | scan | No | Query leakix.net for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-11 | +| myssl | scan | No | Query myssl.com's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2023-07-10 | +| otx | scan | Yes | Query otx.alienvault.com for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-24 | +| passivetotal | scan | Yes | Query the PassiveTotal API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-08 | +| pgp | scan | No | Query common PGP servers for email addresses | email-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS | @TheTechromancer | 2022-08-10 | +| portfilter | scan | No | Filter out unwanted open ports from cloud/CDN targets | passive, safe | OPEN_TCP_PORT, URL, URL_UNVERIFIED | | @TheTechromancer | 2025-01-06 | +| postman | scan | No | Query Postman's API for related workspaces, collections, requests and download them | code-enum, passive, safe, subdomain-enum | ORG_STUB, SOCIAL | CODE_REPOSITORY | @domwhewell-sage | 2024-09-07 | +| postman_download | scan | No | Download workspaces, collections, requests from Postman | code-enum, download, passive, safe, subdomain-enum | CODE_REPOSITORY | FILESYSTEM | @domwhewell-sage | 2024-09-07 | +| rapiddns | scan | No | Query rapiddns.io for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-24 | +| securitytrails | scan | Yes | Query the SecurityTrails API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-03 | +| shodan_dns | scan | Yes | Query Shodan for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-03 | +| shodan_enterprise | scan | Yes | Shodan Enterprise API integration module. | passive, safe | IP_ADDRESS | FINDING, OPEN_TCP_PORT, OPEN_UDP_PORT, TECHNOLOGY | @Control-Punk-Delete | 2026-01-27 | +| shodan_idb | scan | No | Query Shodan's InternetDB for open ports, hostnames, technologies, and vulnerabilities | passive, portscan, safe, subdomain-enum | DNS_NAME, IP_ADDRESS | DNS_NAME, FINDING, OPEN_TCP_PORT, TECHNOLOGY | @TheTechromancer | 2023-12-22 | +| sitedossier | scan | No | Query sitedossier.com for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2023-08-04 | +| skymem | scan | No | Query skymem.info for email addresses | email-enum, passive, safe | DNS_NAME | EMAIL_ADDRESS | @TheTechromancer | 2022-07-11 | +| social | scan | No | Look for social media links in webpages | passive, safe, social-enum | URL_UNVERIFIED | SOCIAL | @TheTechromancer | 2023-03-28 | +| subdomaincenter | scan | No | Query subdomain.center's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2023-07-26 | +| subdomainradar | scan | Yes | Query the Subdomain API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-08 | +| trajan | scan | No | Scans GitHub, GitLab, Azure DevOps, Jenkins, and JFrog for misconfigurations using Praetorian's Trajan tool | code-enum, passive, safe | CODE_REPOSITORY, TECHNOLOGY, URL_UNVERIFIED | FINDING | @N7WERA | 2026-04-11 | +| trickest | scan | Yes | Query Trickest's API for subdomains | affiliates, passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @amiremami | 2024-07-27 | +| trufflehog | scan | No | TruffleHog is a tool for finding credentials | code-enum, passive, safe | CODE_REPOSITORY, FILESYSTEM, HTTP_RESPONSE, RAW_TEXT | FINDING | @domwhewell-sage | 2024-03-12 | +| urlscan | scan | No | Query urlscan.io for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, URL_UNVERIFIED | @TheTechromancer | 2022-06-09 | +| viewdns | scan | No | Query viewdns.info's reverse whois for related domains | affiliates, passive, safe | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-07-04 | +| virustotal | scan | Yes | Query VirusTotal's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME | @TheTechromancer | 2022-08-25 | +| wayback | scan | No | Query archive.org's API for subdomains | passive, safe, subdomain-enum | DNS_NAME | DNS_NAME, URL_UNVERIFIED | @liquidsec | 2022-04-01 | +| asset_inventory | output | No | Merge hosts, open ports, technologies, findings, etc. into a single asset inventory CSV | | DNS_NAME, FINDING, HTTP_RESPONSE, IP_ADDRESS, OPEN_TCP_PORT, TECHNOLOGY, URL, WAF | IP_ADDRESS, OPEN_TCP_PORT | @liquidsec | 2022-09-30 | +| csv | output | No | Output to CSV | | * | | @TheTechromancer | 2022-04-07 | +| discord | output | No | Message a Discord channel when certain events are encountered | | * | | @TheTechromancer | 2023-08-14 | +| elastic | output | No | Send scan results to Elasticsearch | | * | | @TheTechromancer | 2022-11-21 | +| emails | output | No | Output any email addresses found belonging to the target domain | email-enum, safe | EMAIL_ADDRESS | | @domwhewell-sage | 2023-12-23 | +| http | output | No | Send every event to a custom URL via a web request | | * | | @TheTechromancer | 2022-04-13 | +| json | output | No | Output to Newline-Delimited JSON (NDJSON) | | * | | @TheTechromancer | 2022-04-07 | +| kafka | output | No | Output scan data to a Kafka topic | | * | | @TheTechromancer | 2024-11-22 | +| mongo | output | No | Output scan data to a MongoDB database | | * | | @TheTechromancer | 2024-11-17 | +| mysql | output | No | Output scan data to a MySQL database | | * | | @TheTechromancer | 2024-11-13 | +| nats | output | No | Output scan data to a NATS subject | | * | | @TheTechromancer | 2024-11-22 | +| neo4j | output | No | Output to Neo4j | | * | | @TheTechromancer | 2022-04-07 | +| nmap_xml | output | No | Output to Nmap XML | | DNS_NAME, HTTP_RESPONSE, IP_ADDRESS, OPEN_TCP_PORT, PROTOCOL | | @TheTechromancer | 2024-11-16 | +| postgres | output | No | Output scan data to a SQLite database | | * | | @TheTechromancer | 2024-11-08 | +| python | output | No | Output via Python API | | * | | @TheTechromancer | 2022-09-13 | +| rabbitmq | output | No | Output scan data to a RabbitMQ queue | | * | | @TheTechromancer | 2024-11-22 | +| slack | output | No | Message a Slack channel when certain events are encountered | | * | | @TheTechromancer | 2023-08-14 | +| splunk | output | No | Send every event to a splunk instance through HTTP Event Collector | | * | | @w0Tx | 2024-02-17 | +| sqlite | output | No | Output scan data to a SQLite database | | * | | @TheTechromancer | 2024-11-07 | +| stdout | output | No | Output to text | | * | | @TheTechromancer | 2024-04-03 | +| subdomains | output | No | Output only resolved, in-scope subdomains | safe, subdomain-enum | DNS_NAME, DNS_NAME_UNRESOLVED | | @TheTechromancer | 2023-07-31 | +| teams | output | No | Message a Teams channel when certain events are encountered | | * | | @TheTechromancer | 2023-08-14 | +| txt | output | No | Output to text | | * | | @TheTechromancer | 2024-04-03 | +| web_parameters | output | No | Output WEB_PARAMETER names to a file | | WEB_PARAMETER | | @liquidsec | 2025-01-25 | +| web_report | output | No | Create a markdown report with web assets | | FINDING, TECHNOLOGY, URL, VHOST | | @liquidsec | 2023-02-08 | +| websocket | output | No | Output to websockets | | * | | @TheTechromancer | 2022-04-15 | +| zeromq | output | No | Output scan data to a ZeroMQ socket (PUB) | | * | | @TheTechromancer | 2024-11-22 | +| cloudcheck | internal | No | Tag events by cloud provider, identify cloud resources like storage buckets | | * | | @TheTechromancer | 2024-07-07 | +| dnsresolve | internal | No | Perform DNS resolution | | * | DNS_NAME, IP_ADDRESS, RAW_DNS_RECORD | @TheTechromancer | 2022-04-08 | +| aggregate | internal | No | Summarize statistics at the end of a scan | passive, safe | | | @TheTechromancer | 2022-07-25 | +| excavate | internal | No | Passively extract juicy tidbits from scan data | passive, safe | HTTP_RESPONSE, RAW_TEXT | URL_UNVERIFIED, WEB_PARAMETER | @liquidsec | 2022-06-27 | +| speculate | internal | No | Derive certain event types from others by common sense | passive, safe | AZURE_TENANT, DNS_NAME, DNS_NAME_UNRESOLVED, HTTP_RESPONSE, IP_ADDRESS, IP_RANGE, SOCIAL, STORAGE_BUCKET, URL, URL_UNVERIFIED, USERNAME | DNS_NAME, FINDING, IP_ADDRESS, OPEN_TCP_PORT, ORG_STUB | @liquidsec | 2022-05-03 | +| unarchive | internal | No | Extract different types of files into folders on the filesystem | passive, safe | FILESYSTEM | FILESYSTEM | @domwhewell-sage | 2024-12-08 | <!-- END BBOT MODULES --> For a list of module config options, see [Module Options](../scanning/configuration.md#module-config-options). diff --git a/docs/modules/nuclei.md b/docs/modules/nuclei.md index 3858096d78..3264c1ab4b 100644 --- a/docs/modules/nuclei.md +++ b/docs/modules/nuclei.md @@ -7,14 +7,13 @@ BBOT integrates with [Nuclei](https://github.com/projectdiscovery/nuclei), an op ![Nuclei Killchain](https://github.com/blacklanternsecurity/bbot/assets/24899338/7174c4ba-4a6e-4596-bb89-5a0c5f5abe74) -* The BBOT Nuclei module ingests **[URL]** events and emits events of type **[VULNERABILITY]** or **[FINDING]** -* Vulnerabilities will inherit their severity from the Nuclei templates -* Nuclei templates of severity INFO will be emitted as **[FINDINGS]** +* The BBOT Nuclei module ingests **[URL]** events and emits events of type **[FINDING]** +* Findings will inherit their severity from the Nuclei templates ## Default Behavior * By default, only "directory URLs" (URLs ending in a slash) will be scanned, but ALL templates will be used (**BE CAREFUL!**) -* Because it's so aggressive, Nuclei is considered a **deadly** module. This means you need to use the flag **--allow-deadly** to turn it on. +* Because it's aggressive and potentially destructive, Nuclei is tagged as both **loud** and **invasive**. BBOT will warn you before starting the scan, but no special flag is needed to enable it. ## Specifying custom templates @@ -52,7 +51,7 @@ The Nuclei module has many configuration options: | modules.nuclei.silent | bool | Don't display nuclei's banner or status messages | False | | modules.nuclei.tags | str | execute a subset of templates that contain the provided tags | | | modules.nuclei.templates | str | template or template directory paths to include in the scan | | -| modules.nuclei.version | str | nuclei version | 3.7.0 | +| modules.nuclei.version | str | nuclei version | 3.7.1 | <!-- END BBOT MODULE OPTIONS NUCLEI --> Most of these you probably will **NOT** want to change. In particular, we advise against changing the version of Nuclei, as it's possible the latest version won't work right with BBOT. @@ -104,20 +103,20 @@ The **ratelimit** and **concurrency** settings default to the same defaults that ```bash # Scan a SINGLE target with a basic port scan and web modules -bbot -f web-basic -m portscan nuclei --allow-deadly -t app.evilcorp.com +bbot -f web -m portscan nuclei -t app.evilcorp.com ``` ```bash # Scanning MULTIPLE targets -bbot -f web-basic -m portscan nuclei --allow-deadly -t app1.evilcorp.com app2.evilcorp.com app3.evilcorp.com +bbot -f web -m portscan nuclei -t app1.evilcorp.com app2.evilcorp.com app3.evilcorp.com ``` ```bash # Scanning MULTIPLE targets while performing subdomain enumeration -bbot -f subdomain-enum web-basic -m portscan nuclei --allow-deadly -t app1.evilcorp.com app2.evilcorp.com app3.evilcorp.com +bbot -f subdomain-enum web -m portscan nuclei -t app1.evilcorp.com app2.evilcorp.com app3.evilcorp.com ``` ```bash # Scanning MULTIPLE targets on a BUDGET -bbot -f subdomain-enum web-basic -m portscan nuclei --allow-deadly -c modules.nuclei.mode=budget -t app1.evilcorp.com app2.evilcorp.com app3.evilcorp.com +bbot -f subdomain-enum web -m portscan nuclei -c modules.nuclei.mode=budget -t app1.evilcorp.com app2.evilcorp.com app3.evilcorp.com ``` diff --git a/docs/scanning/advanced.md b/docs/scanning/advanced.md index 20afa0535d..ff35952f8e 100644 --- a/docs/scanning/advanced.md +++ b/docs/scanning/advanced.md @@ -32,22 +32,22 @@ if __name__ == "__main__": <!-- BBOT HELP OUTPUT --> ```text -usage: bbot [-h] [-t TARGET [TARGET ...]] [-w WHITELIST [WHITELIST ...]] +usage: bbot [-h] [-t TARGET [TARGET ...]] [-s SEEDS [SEEDS ...]] [-b BLACKLIST [BLACKLIST ...]] [--strict-scope] [-p [PRESET ...]] [-c [CONFIG ...]] [-lp] [-m MODULE [MODULE ...]] [-l] [-lmo] [-em MODULE [MODULE ...]] [-f FLAG [FLAG ...]] [-lf] [-rf FLAG [FLAG ...]] - [-ef FLAG [FLAG ...]] [--allow-deadly] [-n SCAN_NAME] [-v] [-d] - [-s] [--force] [-y] [--fast-mode] [--dry-run] - [--current-preset] [--current-preset-full] [-mh MODULE] - [-o DIR] [-om MODULE [MODULE ...]] [-lo] [--json] [--brief] + [-ef FLAG [FLAG ...]] [-n SCAN_NAME] [-v] [-d] [-S] [--force] + [-y] [--fast-mode] [--dry-run] [--current-preset] + [--current-preset-full] [-mh MODULE] [-o DIR] + [-om MODULE [MODULE ...]] [-lo] [--json] [--brief] [--event-types EVENT_TYPES [EVENT_TYPES ...]] [--exclude-cdn] - [--no-deps | --force-deps | --retry-deps | - --ignore-failed-deps | --install-all-deps] [--version] - [--proxy HTTP_PROXY] [-H CUSTOM_HEADERS [CUSTOM_HEADERS ...]] + [--no-deps | --force-deps | --retry-deps | --ignore-failed-deps] + [--install-all-deps] [--version] [--proxy HTTP_PROXY] + [-H CUSTOM_HEADERS [CUSTOM_HEADERS ...]] [-C CUSTOM_COOKIES [CUSTOM_COOKIES ...]] [--custom-yara-rules CUSTOM_YARA_RULES] - [--user-agent USER_AGENT] + [--user-agent USER_AGENT] [--user-agent-suffix SUFFIX] Bighuge BLS OSINT Tool @@ -55,43 +55,43 @@ options: -h, --help show this help message and exit Target: - -t, --targets TARGET [TARGET ...] - Targets to seed the scan - -w, --whitelist WHITELIST [WHITELIST ...] - What's considered in-scope (by default it's the same as --targets) - -b, --blacklist BLACKLIST [BLACKLIST ...] + -t TARGET [TARGET ...], --targets TARGET [TARGET ...] + Target scope + -s SEEDS [SEEDS ...], --seeds SEEDS [SEEDS ...] + Define seeds to drive passive modules without being in scope (if not specified, defaults to same as targets) + -b BLACKLIST [BLACKLIST ...], --blacklist BLACKLIST [BLACKLIST ...] Don't touch these things - --strict-scope Don't consider subdomains of target/whitelist to be in-scope + --strict-scope Don't consider subdomains of target to be in-scope - exact matches only Presets: - -p, --preset [PRESET ...] + -p [PRESET ...], --preset [PRESET ...] Enable BBOT preset(s) - -c, --config [CONFIG ...] + -c [CONFIG ...], --config [CONFIG ...] Custom config options in key=value format: e.g. 'modules.shodan.api_key=1234' -lp, --list-presets List available presets. Modules: - -m, --modules MODULE [MODULE ...] - Modules to enable. Choices: affiliates,ajaxpro,anubisdb,apkpure,asn,aspnet_bin_exposure,azure_realm,azure_tenant,baddns,baddns_direct,baddns_zone,badsecrets,bevigil,bucket_amazon,bucket_digitalocean,bucket_file_enum,bucket_firebase,bucket_google,bucket_microsoft,bufferoverrun,builtwith,bypass403,c99,censys_dns,censys_ip,certspotter,chaos,code_repository,credshed,crt,crt_db,dehashed,digitorus,dnsbimi,dnsbrute,dnsbrute_mutations,dnscaa,dnscommonsrv,dnsdumpster,dnstlsrpt,docker_pull,dockerhub,dotnetnuke,emailformat,extractous,ffuf,ffuf_shortnames,filedownload,fingerprintx,fullhunt,generic_ssrf,git,git_clone,gitdumper,github_codesearch,github_org,github_usersearch,github_workflows,gitlab_com,gitlab_onprem,google_playstore,gowitness,graphql_introspection,hackertarget,host_header,httpx,hunt,hunterio,iis_shortnames,ip2location,ipneighbor,ipstack,jadx,leakix,legba,lightfuzz,medusa,myssl,newsletters,ntlm,nuclei,oauth,otx,paramminer_cookies,paramminer_getparams,paramminer_headers,passivetotal,pgp,portfilter,portscan,postman,postman_download,rapiddns,reflected_parameters,retirejs,robots,securitytrails,securitytxt,shodan_dns,shodan_idb,sitedossier,skymem,smuggler,social,sslcert,subdomaincenter,subdomainradar,telerik,trickest,trufflehog,url_manipulation,urlscan,vhost,viewdns,virustotal,wafw00f,wayback,wpscan + -m MODULE [MODULE ...], --modules MODULE [MODULE ...] + Modules to enable. Choices: affiliates,ajaxpro,anubisdb,apkpure,asn,aspnet_bin_exposure,azure_tenant,baddns,baddns_direct,baddns_zone,badsecrets,bevigil,bucket_amazon,bucket_digitalocean,bucket_file_enum,bucket_firebase,bucket_google,bucket_microsoft,bufferoverrun,builtwith,bypass403,c99,censys_dns,censys_ip,certspotter,chaos,code_repository,credshed,crt,crt_db,dehashed,digitorus,dnsbimi,dnsbrute,dnsbrute_mutations,dnscaa,dnscommonsrv,dnsdumpster,dnstlsrpt,docker_pull,dockerhub,dotnetnuke,emailformat,ffuf,ffuf_shortnames,filedownload,fingerprintx,fullhunt,git,git_clone,gitdumper,github_codesearch,github_org,github_usersearch,github_workflows,gitlab_com,gitlab_onprem,google_playstore,gowitness,graphql_introspection,hackertarget,host_header,httpx,hunt,hunterio,iis_shortnames,ip2location,ipneighbor,ipstack,jadx,kreuzberg,leakix,legba,lightfuzz,medusa,myssl,newsletters,ntlm,nuclei,oauth,otx,paramminer_cookies,paramminer_getparams,paramminer_headers,passivetotal,pgp,portfilter,portscan,postman,postman_download,rapiddns,reflected_parameters,retirejs,robots,securitytrails,securitytxt,shodan_dns,shodan_enterprise,shodan_idb,sitedossier,skymem,smuggler,social,sslcert,subdomaincenter,subdomainradar,telerik,trajan,trickest,trufflehog,url_manipulation,urlscan,viewdns,virustotal,wafw00f,wayback,wpscan -l, --list-modules List available modules. -lmo, --list-module-options Show all module config options - -em, --exclude-modules MODULE [MODULE ...] + -em MODULE [MODULE ...], --exclude-modules MODULE [MODULE ...] Exclude these modules. - -f, --flags FLAG [FLAG ...] - Enable modules by flag. Choices: active,affiliates,aggressive,baddns,cloud-enum,code-enum,deadly,download,email-enum,iis-shortnames,passive,portscan,safe,service-enum,slow,social-enum,subdomain-enum,subdomain-hijack,web-basic,web-paramminer,web-screenshots,web-thorough + -f FLAG [FLAG ...], --flags FLAG [FLAG ...] + Enable modules by flag. Choices: active,affiliates,baddns,cloud-enum,code-enum,download,email-enum,iis-shortnames,invasive,loud,passive,portscan,safe,service-enum,slow,social-enum,subdomain-enum,subdomain-hijack,web,web-heavy,web-paramminer,web-screenshots -lf, --list-flags List available flags. - -rf, --require-flags FLAG [FLAG ...] + -rf FLAG [FLAG ...], --require-flags FLAG [FLAG ...] Only enable modules with these flags (e.g. -rf passive) - -ef, --exclude-flags FLAG [FLAG ...] - Disable modules with these flags. (e.g. -ef aggressive) - --allow-deadly Enable the use of highly aggressive modules + -ef FLAG [FLAG ...], --exclude-flags FLAG [FLAG ...] + Disable modules with these flags. (e.g. -ef loud) Scan: - -n, --name SCAN_NAME Name of scan (default: random) + -n SCAN_NAME, --name SCAN_NAME + Name of scan (default: random) -v, --verbose Be more verbose -d, --debug Enable debugging - -s, --silent Be quiet + -S, --silent Be quiet --force Run scan even in the case of condition violations or failed module setups -y, --yes Skip scan confirmation prompt --fast-mode Scan only the provided targets as fast as possible, with no extra discovery @@ -99,13 +99,14 @@ Scan: --current-preset Show the current preset in YAML format --current-preset-full Show the current preset in its full form, including defaults - -mh, --module-help MODULE + -mh MODULE, --module-help MODULE Show help for a specific module Output: - -o, --output-dir DIR Directory to output scan results - -om, --output-modules MODULE [MODULE ...] - Output module(s). Choices: asset_inventory,csv,discord,emails,http,json,mysql,neo4j,nmap_xml,postgres,python,slack,splunk,sqlite,stdout,subdomains,teams,txt,web_parameters,web_report,websocket + -o DIR, --output-dir DIR + Directory to output scan results + -om MODULE [MODULE ...], --output-modules MODULE [MODULE ...] + Output module(s). Choices: asset_inventory,csv,discord,elastic,emails,http,json,kafka,mongo,mysql,nats,neo4j,nmap_xml,postgres,python,rabbitmq,slack,splunk,sqlite,stdout,subdomains,teams,txt,web_parameters,web_report,websocket,zeromq -lo, --list-output-modules List available output modules --json, -j Output scan data in JSON format @@ -126,14 +127,16 @@ Module dependencies: Misc: --version show BBOT version and exit --proxy HTTP_PROXY Use this proxy for all HTTP requests - -H, --custom-headers CUSTOM_HEADERS [CUSTOM_HEADERS ...] + -H CUSTOM_HEADERS [CUSTOM_HEADERS ...], --custom-headers CUSTOM_HEADERS [CUSTOM_HEADERS ...] List of custom headers as key value pairs (header=value). - -C, --custom-cookies CUSTOM_COOKIES [CUSTOM_COOKIES ...] + -C CUSTOM_COOKIES [CUSTOM_COOKIES ...], --custom-cookies CUSTOM_COOKIES [CUSTOM_COOKIES ...] List of custom cookies as key value pairs (cookie=value). - --custom-yara-rules, -cy CUSTOM_YARA_RULES + --custom-yara-rules CUSTOM_YARA_RULES, -cy CUSTOM_YARA_RULES Add custom yara rules to excavate - --user-agent, -ua USER_AGENT + --user-agent USER_AGENT, -ua USER_AGENT Set the user-agent for all HTTP requests + --user-agent-suffix SUFFIX, -uas SUFFIX + Suffix to append to the user-agent EXAMPLES @@ -147,7 +150,7 @@ EXAMPLES bbot -t evilcorp.com -p subdomain-enum -m portscan gowitness -n my_scan -o . Subdomains + basic web scan: - bbot -t evilcorp.com -p subdomain-enum web-basic + bbot -t evilcorp.com -p subdomain-enum web Web spider: bbot -t www.evilcorp.com -p spider -c web.spider_distance=2 web.spider_depth=2 diff --git a/docs/scanning/configuration.md b/docs/scanning/configuration.md index 2542a0c8dd..bbc5aa7a22 100644 --- a/docs/scanning/configuration.md +++ b/docs/scanning/configuration.md @@ -30,7 +30,7 @@ You can specify config options either via the command line or the config. For ex bbot -t evilcorp.com -c http_proxy=http://127.0.0.1:8080 ``` -Or, in `~/.config/bbot/config.yml`: +Or, in `~/.config/bbot/bbot.yml`: ```yaml title="~/.bbot/config/bbot.yml" http_proxy: http://127.0.0.1:8080 @@ -59,6 +59,15 @@ Below is a full list of the config options supported, along with their defaults. ```yaml title="defaults.yml" ### BASIC OPTIONS ### +# NOTE: If used in a preset, these options must be nested underneath "config:" like so: +# config: +# home: ~/.bbot +# keep_scans: 20 +# scope: +# strict: true +# dns: +# minimal: true + # BBOT working directory home: ~/.bbot # How many scan results to keep before cleaning up the older ones @@ -74,7 +83,7 @@ folder_blobs: false scope: # strict scope means only exact DNS names are considered in-scope - # subdomains are not included unless they are explicitly provided in the target list + # their subdomains are not included unless explicitly added to the target strict: false # Filter by scope distance which events are displayed in the output # 0 == show only in-scope events (affiliates are always shown) @@ -87,7 +96,7 @@ scope: ### DNS ### dns: - # Completely disable DNS resolution (careful if you have IP whitelists/blacklists, consider using minimal=true instead) + # Completely disable DNS resolution (careful if you have IP targets/blacklists, consider using minimal=true instead) disable: false # Speed up scan by not creating any new DNS events, and only resolving A and AAAA records minimal: false @@ -136,6 +145,8 @@ web: http_proxy: # Web user-agent user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.2151.97 + # Suffix to append to user-agent (e.g. for tracking or identification) + user_agent_suffix: # Set the maximum number of HTTP links that can be followed in a row (0 == no spidering allowed) spider_distance: 0 # Set the maximum directory depth for the web spider @@ -305,6 +316,7 @@ parameter_blacklist: - __SCROLLPOSITIONY - __SCROLLPOSITIONX - ASP.NET_SessionId + - .AspNetCore.Session - PHPSESSID - __cf_bm - f5_cspm @@ -312,6 +324,7 @@ parameter_blacklist: parameter_blacklist_prefixes: - TS01 - BIGipServer + - f5avr - incap_ - visid_incap_ - AWSALB @@ -361,10 +374,14 @@ In addition to the stated options for each module, the following universal optio |-----------------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | modules.baddns.custom_nameservers | list | Force BadDNS to use a list of custom nameservers | [] | | modules.baddns.enabled_submodules | list | A list of submodules to enable. Empty list (default) enables CNAME, TXT and MX Only | [] | -| modules.baddns.only_high_confidence | bool | Do not emit low-confidence or generic detections | False | +| modules.baddns.min_confidence | str | Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED) | MODERATE | +| modules.baddns.min_severity | str | Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL) | LOW | | modules.baddns_direct.custom_nameservers | list | Force BadDNS to use a list of custom nameservers | [] | +| modules.baddns_direct.min_confidence | str | Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED) | MODERATE | +| modules.baddns_direct.min_severity | str | Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL) | LOW | | modules.baddns_zone.custom_nameservers | list | Force BadDNS to use a list of custom nameservers | [] | -| modules.baddns_zone.only_high_confidence | bool | Do not emit low-confidence or generic detections | False | +| modules.baddns_zone.min_confidence | str | Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED) | MODERATE | +| modules.baddns_zone.min_severity | str | Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL) | INFO | | modules.badsecrets.custom_secrets | NoneType | Include custom secrets loaded from a local file | None | | modules.bucket_amazon.permutations | bool | Whether to try permutations | False | | modules.bucket_digitalocean.permutations | bool | Whether to try permutations | False | @@ -396,7 +413,6 @@ In addition to the stated options for each module, the following universal optio | modules.filedownload.output_folder | str | Folder to download files to. If not specified, downloaded files will be deleted when the scan completes, to minimize disk usage. | | | modules.fingerprintx.skip_common_web | bool | Skip common web ports such as 80, 443, 8080, 8443, etc. | True | | modules.fingerprintx.version | str | fingerprintx version | 1.1.4 | -| modules.generic_ssrf.skip_dns_interaction | bool | Do not report DNS interactions (only HTTP interaction) | False | | modules.gitlab_com.api_key | str | GitLab access token (for gitlab.com/org only) | | | modules.gitlab_onprem.api_key | str | GitLab access token (for self-hosted instances only) | | | modules.gowitness.chrome_path | str | Path to chrome executable | | @@ -407,7 +423,7 @@ In addition to the stated options for each module, the following universal optio | modules.gowitness.social | bool | Whether to screenshot social media webpages | False | | modules.gowitness.threads | int | How many gowitness threads to spawn (default is number of CPUs x 2) | 0 | | modules.gowitness.timeout | int | Preflight check timeout | 10 | -| modules.gowitness.version | str | Gowitness version | 3.0.5 | +| modules.gowitness.version | str | Gowitness version | 3.1.1 | | modules.graphql_introspection.graphql_endpoint_urls | list | List of GraphQL endpoint to suffix to the target URL | ['/', '/graphql', '/v1/graphql'] | | modules.graphql_introspection.output_folder | str | Folder to save the GraphQL schemas to | | | modules.httpx.in_scope_only | bool | Only visit web reparents that are in scope. | True | @@ -431,7 +447,7 @@ In addition to the stated options for each module, the following universal optio | modules.legba.vnc_wordlist | str | Wordlist URL for VNC password wordlist, newline separated | https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Default-Credentials/vnc-betterdefaultpasslist.txt | | modules.lightfuzz.avoid_wafs | bool | Avoid running against confirmed WAFs, which are likely to block lightfuzz requests | True | | modules.lightfuzz.disable_post | bool | Disable processing of POST parameters, avoiding form submissions. | False | -| modules.lightfuzz.enabled_submodules | list | A list of submodules to enable. Empty list enabled all modules. | ['sqli', 'cmdi', 'xss', 'path', 'ssti', 'crypto', 'serial', 'esi'] | +| modules.lightfuzz.enabled_submodules | list | A list of submodules to enable. Empty list enabled all modules. | ['sqli', 'cmdi', 'xss', 'path', 'ssti', 'crypto', 'serial', 'esi', 'ssrf'] | | modules.lightfuzz.force_common_headers | bool | Force emit commonly exploitable parameters that may be difficult to detect | False | | modules.lightfuzz.try_get_as_post | bool | For each GETPARAM, also fuzz it as a POSTPARAM (in addition to normal GET fuzzing). | False | | modules.lightfuzz.try_post_as_get | bool | For each POSTPARAM, also fuzz it as a GETPARAM (in addition to normal POST fuzzing). | False | @@ -454,7 +470,7 @@ In addition to the stated options for each module, the following universal optio | modules.nuclei.silent | bool | Don't display nuclei's banner or status messages | False | | modules.nuclei.tags | str | execute a subset of templates that contain the provided tags | | | modules.nuclei.templates | str | template or template directory paths to include in the scan | | -| modules.nuclei.version | str | nuclei version | 3.7.0 | +| modules.nuclei.version | str | nuclei version | 3.7.1 | | modules.oauth.try_all | bool | Check for OAUTH/IODC on every subdomain and URL. | False | | modules.paramminer_cookies.recycle_words | bool | Attempt to use words found during the scan on all other endpoints | False | | modules.paramminer_cookies.skip_boring_words | bool | Remove commonly uninteresting words from the wordlist | True | @@ -489,9 +505,6 @@ In addition to the stated options for each module, the following universal optio | modules.telerik.exploit_RAU_crypto | bool | Attempt to confirm any RAU AXD detections are vulnerable | False | | modules.telerik.include_subdirs | bool | Include subdirectories in the scan (off by default) | False | | modules.url_manipulation.allow_redirects | bool | Allowing redirects will sometimes create false positives. Disallowing will sometimes create false negatives. Allowed by default. | True | -| modules.vhost.force_basehost | str | Use a custom base host (e.g. evilcorp.com) instead of the default behavior of using the current URL | | -| modules.vhost.lines | int | take only the first N lines from the wordlist when finding directories | 5000 | -| modules.vhost.wordlist | str | Wordlist containing subdomains | https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/subdomains-top1million-5000.txt | | modules.wafw00f.generic_detect | bool | When no specific WAF detections are made, try to perform a generic detect | True | | modules.wpscan.api_key | str | WPScan API Key | | | modules.wpscan.connection_timeout | int | The connection timeout in seconds (default 2) | 2 | @@ -532,7 +545,6 @@ In addition to the stated options for each module, the following universal optio | modules.dnstlsrpt.emit_urls | bool | Emit URL_UNVERIFIED events | True | | modules.docker_pull.all_tags | bool | Download all tags from each registry (Default False) | False | | modules.docker_pull.output_folder | str | Folder to download docker repositories to. If not specified, downloaded docker images will be deleted when the scan completes, to minimize disk usage. | | -| modules.extractous.extensions | list | File extensions to parse | ['bak', 'bash', 'bashrc', 'conf', 'cfg', 'crt', 'csv', 'db', 'sqlite', 'doc', 'docx', 'ica', 'indd', 'ini', 'json', 'key', 'pub', 'log', 'markdown', 'md', 'odg', 'odp', 'ods', 'odt', 'pdf', 'pem', 'pps', 'ppsx', 'ppt', 'pptx', 'ps1', 'rdp', 'rsa', 'sh', 'sql', 'swp', 'sxw', 'txt', 'vbs', 'wpd', 'xls', 'xlsx', 'xml', 'yml', 'yaml'] | | modules.fullhunt.api_key | str | FullHunt API Key | | | modules.git_clone.api_key | str | Github token | | | modules.git_clone.output_folder | str | Folder to clone repositories to. If not specified, cloned repositories will be deleted when the scan completes, to minimize disk usage. | | @@ -554,21 +566,32 @@ In addition to the stated options for each module, the following universal optio | modules.ipneighbor.num_bits | int | Netmask size (in CIDR notation) to check. Default is 4 bits (16 hosts) | 4 | | modules.ipstack.api_key | str | IPStack GeoIP API Key | | | modules.jadx.threads | int | Maximum jadx threads for extracting apk's, default: 4 | 4 | +| modules.kreuzberg.extensions | list | File extensions to parse | ['bak', 'bash', 'bashrc', 'conf', 'cfg', 'crt', 'csv', 'db', 'sqlite', 'doc', 'docx', 'ica', 'indd', 'ini', 'json', 'key', 'pub', 'log', 'markdown', 'md', 'odg', 'odp', 'ods', 'odt', 'pdf', 'pem', 'pps', 'ppsx', 'ppt', 'pptx', 'ps1', 'rdp', 'rsa', 'sh', 'sql', 'swp', 'sxw', 'txt', 'vbs', 'wpd', 'xls', 'xlsx', 'xml', 'yml', 'yaml'] | | modules.leakix.api_key | str | LeakIX API Key | | | modules.otx.api_key | str | OTX API key | | | modules.passivetotal.api_key | str | PassiveTotal API Key in the format of 'username:api_key' | | | modules.pgp.search_urls | list | PGP key servers to search |` ['https://keyserver.ubuntu.com/pks/lookup?fingerprint=on&op=vindex&search=<query>', 'http://the.earth.li:11371/pks/lookup?fingerprint=on&op=vindex&search=<query>', 'https://pgpkeys.eu/pks/lookup?search=<query>&op=index', 'https://pgp.mit.edu/pks/lookup?search=<query>&op=index'] `| | modules.portfilter.allowed_cdn_ports | str | Comma-separated list of ports that are allowed to be scanned for CDNs | 80,443 | -| modules.portfilter.cdn_tags | str | Comma-separated list of tags to skip, e.g. 'cdn,cloud' | cdn- | +| modules.portfilter.cdn_tags | str | Comma-separated list of tags to skip, e.g. 'cdn,waf' | cdn,waf | | modules.postman.api_key | str | Postman API Key | | | modules.postman_download.api_key | str | Postman API Key | | | modules.postman_download.output_folder | str | Folder to download postman workspaces to. If not specified, downloaded workspaces will be deleted when the scan completes, to minimize disk usage. | | | modules.securitytrails.api_key | str | SecurityTrails API key | | | modules.shodan_dns.api_key | str | Shodan API key | | +| modules.shodan_enterprise.api_key | str | Shodan API Key | | +| modules.shodan_enterprise.in_scope_only | bool | Only query in-scope IPs. If False, will query up to distance 1. | True | | modules.shodan_idb.retries | NoneType | How many times to retry API requests (e.g. after a 429 error). Overrides the global web.api_retries setting. | None | | modules.subdomainradar.api_key | str | SubDomainRadar.io API key | | | modules.subdomainradar.group | str | The enumeration group to use. Choose from fast, medium, deep | fast | | modules.subdomainradar.timeout | int | Timeout in seconds | 120 | +| modules.trajan.ado_token | str | Azure DevOps Personal Access Token (PAT) | | +| modules.trajan.github_token | str | GitHub API token for rate-limiting and private repo access | | +| modules.trajan.gitlab_token | str | GitLab API token for private repo access | | +| modules.trajan.jenkins_password | str | Jenkins password for basic auth | | +| modules.trajan.jenkins_token | str | Jenkins API token | | +| modules.trajan.jenkins_username | str | Jenkins username for basic auth | | +| modules.trajan.jfrog_token | str | JFrog API token | | +| modules.trajan.version | str | Trajan version to download and use | 1.0.0 | | modules.trickest.api_key | str | Trickest API key | | | modules.trufflehog.concurrency | int | Number of concurrent workers | 8 | | modules.trufflehog.config | str | File path or URL to YAML trufflehog config | | @@ -584,25 +607,37 @@ In addition to the stated options for each module, the following universal optio | modules.asset_inventory.summary_netmask | int | Subnet mask to use when summarizing IP addresses at end of scan | 16 | | modules.asset_inventory.use_previous | bool |` Emit previous asset inventory as new events (use in conjunction with -n <old_scan_name>) `| False | | modules.csv.output_file | str | Output to CSV file | | -| modules.discord.event_types | list | Types of events to send | ['VULNERABILITY', 'FINDING'] | -| modules.discord.min_severity | str | Only allow VULNERABILITY events of this severity or higher | LOW | +| modules.discord.event_types | list | Types of events to send | ['FINDING'] | +| modules.discord.min_severity | str | Only allow FINDING events of this severity or higher | LOW | | modules.discord.retries | int | Number of times to retry sending the message before skipping the event | 10 | | modules.discord.webhook_url | str | Discord webhook URL | | +| modules.elastic.password | str | Elastic password | bbotislife | +| modules.elastic.timeout | int | HTTP timeout | 10 | +| modules.elastic.url | str |` Elastic URL (e.g. https://localhost:9200/<your_index>/_doc) `| https://localhost:9200/bbot_events/_doc | +| modules.elastic.username | str | Elastic username | elastic | | modules.emails.output_file | str | Output to file | | | modules.http.bearer | str | Authorization Bearer token | | +| modules.http.headers | dict | Additional headers to send with the request | {} | | modules.http.method | str | HTTP method | POST | | modules.http.password | str | Password (basic auth) | | -| modules.http.siem_friendly | bool | Format JSON in a SIEM-friendly way for ingestion into Elastic, Splunk, etc. | False | | modules.http.timeout | int | HTTP timeout | 10 | | modules.http.url | str | Web URL | | | modules.http.username | str | Username (basic auth) | | | modules.json.output_file | str | Output to file | | -| modules.json.siem_friendly | bool | Output JSON in a SIEM-friendly format for ingestion into Elastic, Splunk, etc. | False | +| modules.kafka.bootstrap_servers | str | A comma-separated list of Kafka server addresses | localhost:9092 | +| modules.kafka.topic | str | The Kafka topic to publish events to | bbot_events | +| modules.mongo.collection_prefix | str | Prefix the name of each collection with this string | | +| modules.mongo.database | str | The name of the database to use | bbot | +| modules.mongo.password | str | The password to use to connect to the database | | +| modules.mongo.uri | str | The URI of the MongoDB server | mongodb://localhost:27017 | +| modules.mongo.username | str | The username to use to connect to the database | | | modules.mysql.database | str | The database name to connect to | bbot | | modules.mysql.host | str | The server running MySQL | localhost | | modules.mysql.password | str | The password to connect to MySQL | bbotislife | | modules.mysql.port | int | The port to connect to MySQL | 3306 | | modules.mysql.username | str | The username to connect to MySQL | root | +| modules.nats.servers | list | A list of NATS server addresses | [] | +| modules.nats.subject | str | The NATS subject to publish events to | bbot_events | | modules.neo4j.password | str | Neo4j password | bbotislife | | modules.neo4j.uri | str | Neo4j server + port | bolt://localhost:7687 | | modules.neo4j.username | str | Neo4j username | neo4j | @@ -611,8 +646,10 @@ In addition to the stated options for each module, the following universal optio | modules.postgres.password | str | The password to connect to Postgres | bbotislife | | modules.postgres.port | int | The port to connect to Postgres | 5432 | | modules.postgres.username | str | The username to connect to Postgres | postgres | -| modules.slack.event_types | list | Types of events to send | ['VULNERABILITY', 'FINDING'] | -| modules.slack.min_severity | str | Only allow VULNERABILITY events of this severity or higher | LOW | +| modules.rabbitmq.queue | str | The RabbitMQ queue to publish events to | bbot_events | +| modules.rabbitmq.url | str | The RabbitMQ connection URL | amqp://guest:guest@localhost/ | +| modules.slack.event_types | list | Types of events to send | ['FINDING'] | +| modules.slack.min_severity | str | Only allow FINDING events of this severity or higher | LOW | | modules.slack.retries | int | Number of times to retry sending the message before skipping the event | 10 | | modules.slack.webhook_url | str | Discord webhook URL | | | modules.splunk.hectoken | str | HEC Token | | @@ -628,8 +665,8 @@ In addition to the stated options for each module, the following universal optio | modules.stdout.in_scope_only | bool | Whether to only show in-scope events | False | | modules.subdomains.include_unresolved | bool | Include unresolved subdomains in output | False | | modules.subdomains.output_file | str | Output to file | | -| modules.teams.event_types | list | Types of events to send | ['VULNERABILITY', 'FINDING'] | -| modules.teams.min_severity | str | Only allow VULNERABILITY events of this severity or higher | LOW | +| modules.teams.event_types | list | Types of events to send | ['FINDING'] | +| modules.teams.min_severity | str | Only allow FINDING events of this severity or higher | LOW | | modules.teams.retries | int | Number of times to retry sending the message before skipping the event | 10 | | modules.teams.webhook_url | str | Teams webhook URL | | | modules.txt.output_file | str | Output to file | | @@ -641,10 +678,11 @@ In addition to the stated options for each module, the following universal optio | modules.websocket.preserve_graph | bool | Preserve full chains of events in the graph (prevents orphans) | True | | modules.websocket.token | str | Authorization Bearer token | | | modules.websocket.url | str | Web URL | | +| modules.zeromq.zmq_address | str | The ZeroMQ socket address to publish events to (e.g. tcp://localhost:5555) | | | modules.excavate.custom_yara_rules | str | Include custom Yara rules | | | modules.excavate.speculate_params | bool | Enable speculative parameter extraction from JSON and XML content | False | | modules.excavate.yara_max_match_data | int | Sets the maximum amount of text that can extracted from a YARA regex | 2000 | | modules.speculate.essential_only | bool | Only enable essential speculate features (no extra discovery) | False | -| modules.speculate.max_hosts | int | Max number of IP_RANGE hosts to convert into IP_ADDRESS events | 65536 | +| modules.speculate.ip_range_max_hosts | int | Max number of hosts an IP_RANGE can contain to allow conversion into IP_ADDRESS events | 65536 | | modules.speculate.ports | str | The set of ports to speculate on | 80,443 | <!-- END BBOT MODULE OPTIONS --> diff --git a/docs/scanning/events.md b/docs/scanning/events.md index f554b8b797..0569db7083 100644 --- a/docs/scanning/events.md +++ b/docs/scanning/events.md @@ -104,56 +104,50 @@ Below is a full list of event types along with which modules produce/consume the ## List of Event Types <!-- BBOT EVENTS --> -| Event Type | # Consuming Modules | # Producing Modules | Consuming Modules | Producing Modules | -|---------------------|-----------------------|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| * | 18 | 0 | affiliates, cloudcheck, csv, discord, dnsresolve, http, json, mysql, neo4j, postgres, python, slack, splunk, sqlite, stdout, teams, txt, websocket | | -| ASN | 0 | 1 | | asn | -| AZURE_TENANT | 1 | 0 | speculate | | -| CODE_REPOSITORY | 7 | 8 | docker_pull, git_clone, gitdumper, github_workflows, google_playstore, postman_download, trufflehog | code_repository, dockerhub, git, github_codesearch, github_org, gitlab_com, gitlab_onprem, postman | -| DNS_NAME | 60 | 43 | anubisdb, asset_inventory, azure_realm, azure_tenant, baddns, baddns_zone, bevigil, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, credshed, crt, crt_db, dehashed, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, emailformat, fullhunt, github_codesearch, github_usersearch, hackertarget, hunterio, leakix, myssl, nmap_xml, oauth, otx, passivetotal, pgp, portscan, rapiddns, securitytrails, securitytxt, shodan_dns, shodan_idb, sitedossier, skymem, speculate, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, viewdns, virustotal, wayback | anubisdb, azure_tenant, bevigil, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, crt, crt_db, digitorus, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnsresolve, fullhunt, hackertarget, hunterio, leakix, myssl, ntlm, oauth, otx, passivetotal, rapiddns, securitytrails, shodan_dns, shodan_idb, sitedossier, speculate, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, vhost, viewdns, virustotal, wayback | -| DNS_NAME_UNRESOLVED | 3 | 0 | baddns, speculate, subdomains | | -| EMAIL_ADDRESS | 1 | 11 | emails | credshed, dehashed, dnscaa, dnstlsrpt, emailformat, github_usersearch, hunterio, pgp, securitytxt, skymem, sslcert | -| FILESYSTEM | 4 | 9 | extractous, jadx, trufflehog, unarchive | apkpure, docker_pull, filedownload, git_clone, gitdumper, github_workflows, jadx, postman_download, unarchive | -| FINDING | 2 | 32 | asset_inventory, web_report | ajaxpro, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_microsoft, bypass403, git, gitlab_onprem, graphql_introspection, host_header, hunt, legba, lightfuzz, newsletters, ntlm, nuclei, paramminer_cookies, paramminer_getparams, reflected_parameters, retirejs, shodan_idb, smuggler, speculate, telerik, trufflehog, url_manipulation, wpscan | -| GEOLOCATION | 0 | 2 | | ip2location, ipstack | -| HASHED_PASSWORD | 0 | 2 | | credshed, dehashed | -| HTTP_RESPONSE | 18 | 1 | ajaxpro, asset_inventory, badsecrets, dotnetnuke, excavate, filedownload, gitlab_onprem, host_header, newsletters, nmap_xml, ntlm, paramminer_cookies, paramminer_getparams, paramminer_headers, speculate, telerik, trufflehog, wpscan | httpx | -| IP_ADDRESS | 10 | 5 | asn, asset_inventory, censys_ip, ip2location, ipneighbor, ipstack, nmap_xml, portscan, shodan_idb, speculate | asset_inventory, censys_ip, dnsresolve, ipneighbor, speculate | -| IP_RANGE | 2 | 0 | portscan, speculate | | -| MOBILE_APP | 1 | 1 | apkpure | google_playstore | -| OPEN_TCP_PORT | 6 | 5 | asset_inventory, fingerprintx, httpx, nmap_xml, portfilter, sslcert | asset_inventory, censys_ip, portscan, shodan_idb, speculate | -| OPEN_UDP_PORT | 0 | 1 | | censys_ip | -| ORG_STUB | 4 | 1 | dockerhub, github_org, google_playstore, postman | speculate | -| PASSWORD | 0 | 2 | | credshed, dehashed | -| PROTOCOL | 3 | 2 | legba, medusa, nmap_xml | censys_ip, fingerprintx | -| RAW_DNS_RECORD | 0 | 3 | | dnsbimi, dnsresolve, dnstlsrpt | -| RAW_TEXT | 2 | 1 | excavate, trufflehog | extractous | -| SOCIAL | 7 | 4 | dockerhub, github_org, gitlab_com, gitlab_onprem, gowitness, postman, speculate | dockerhub, github_usersearch, gitlab_onprem, social | -| STORAGE_BUCKET | 8 | 5 | baddns_direct, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_microsoft, speculate | bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_microsoft | -| TECHNOLOGY | 4 | 8 | asset_inventory, gitlab_onprem, web_report, wpscan | badsecrets, censys_ip, dotnetnuke, gitlab_onprem, gowitness, nuclei, shodan_idb, wpscan | -| URL | 24 | 2 | ajaxpro, aspnet_bin_exposure, asset_inventory, baddns_direct, bypass403, ffuf, generic_ssrf, git, gowitness, graphql_introspection, httpx, iis_shortnames, lightfuzz, ntlm, nuclei, portfilter, robots, smuggler, speculate, telerik, url_manipulation, vhost, wafw00f, web_report | gowitness, httpx | -| URL_HINT | 1 | 1 | ffuf_shortnames | iis_shortnames | -| URL_UNVERIFIED | 8 | 19 | code_repository, filedownload, httpx, oauth, portfilter, retirejs, social, speculate | azure_realm, bevigil, bucket_file_enum, censys_ip, dnsbimi, dnscaa, dnstlsrpt, dockerhub, excavate, ffuf, ffuf_shortnames, github_codesearch, gowitness, hunterio, robots, securitytxt, urlscan, wayback, wpscan | -| USERNAME | 1 | 2 | speculate | credshed, dehashed | -| VHOST | 1 | 1 | web_report | vhost | -| VULNERABILITY | 2 | 15 | asset_inventory, web_report | ajaxpro, aspnet_bin_exposure, baddns, baddns_direct, baddns_zone, badsecrets, dotnetnuke, generic_ssrf, lightfuzz, medusa, nuclei, shodan_idb, telerik, trufflehog, wpscan | -| WAF | 1 | 1 | asset_inventory | wafw00f | -| WEBSCREENSHOT | 0 | 1 | | gowitness | -| WEB_PARAMETER | 7 | 4 | hunt, lightfuzz, paramminer_cookies, paramminer_getparams, paramminer_headers, reflected_parameters, web_parameters | excavate, paramminer_cookies, paramminer_getparams, paramminer_headers | +| Event Type | # Consuming Modules | # Producing Modules | Consuming Modules | Producing Modules | +|---------------------|-----------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| * | 24 | 0 | affiliates, cloudcheck, csv, discord, dnsresolve, elastic, http, json, kafka, mongo, mysql, nats, neo4j, postgres, python, rabbitmq, slack, splunk, sqlite, stdout, teams, txt, websocket, zeromq | | +| ASN | 0 | 1 | | asn | +| AZURE_TENANT | 1 | 1 | speculate | azure_tenant | +| CODE_REPOSITORY | 8 | 8 | docker_pull, git_clone, gitdumper, github_workflows, google_playstore, postman_download, trajan, trufflehog | code_repository, dockerhub, git, github_codesearch, github_org, gitlab_com, gitlab_onprem, postman | +| DNS_NAME | 59 | 42 | anubisdb, asset_inventory, azure_tenant, baddns, baddns_zone, bevigil, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, credshed, crt, crt_db, dehashed, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, emailformat, fullhunt, github_codesearch, github_usersearch, hackertarget, hunterio, leakix, myssl, nmap_xml, oauth, otx, passivetotal, pgp, portscan, rapiddns, securitytrails, securitytxt, shodan_dns, shodan_idb, sitedossier, skymem, speculate, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, viewdns, virustotal, wayback | anubisdb, azure_tenant, bevigil, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, crt, crt_db, digitorus, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnsresolve, fullhunt, hackertarget, hunterio, leakix, myssl, ntlm, oauth, otx, passivetotal, rapiddns, securitytrails, shodan_dns, shodan_idb, sitedossier, speculate, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, viewdns, virustotal, wayback | +| DNS_NAME_UNRESOLVED | 3 | 0 | baddns, speculate, subdomains | | +| EMAIL_ADDRESS | 1 | 11 | emails | credshed, dehashed, dnscaa, dnstlsrpt, emailformat, github_usersearch, hunterio, pgp, securitytxt, skymem, sslcert | +| FILESYSTEM | 4 | 9 | jadx, kreuzberg, trufflehog, unarchive | apkpure, docker_pull, filedownload, git_clone, gitdumper, github_workflows, jadx, postman_download, unarchive | +| FINDING | 2 | 36 | asset_inventory, web_report | ajaxpro, aspnet_bin_exposure, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_microsoft, bypass403, dotnetnuke, git, gitlab_onprem, graphql_introspection, host_header, hunt, legba, lightfuzz, medusa, newsletters, ntlm, nuclei, reflected_parameters, retirejs, shodan_enterprise, shodan_idb, smuggler, speculate, telerik, trajan, trufflehog, url_manipulation, wpscan | +| GEOLOCATION | 0 | 2 | | ip2location, ipstack | +| HASHED_PASSWORD | 0 | 2 | | credshed, dehashed | +| HTTP_RESPONSE | 18 | 1 | ajaxpro, asset_inventory, badsecrets, dotnetnuke, excavate, filedownload, gitlab_onprem, host_header, newsletters, nmap_xml, ntlm, paramminer_cookies, paramminer_getparams, paramminer_headers, speculate, telerik, trufflehog, wpscan | httpx | +| IP_ADDRESS | 11 | 5 | asn, asset_inventory, censys_ip, ip2location, ipneighbor, ipstack, nmap_xml, portscan, shodan_enterprise, shodan_idb, speculate | asset_inventory, censys_ip, dnsresolve, ipneighbor, speculate | +| IP_RANGE | 2 | 0 | portscan, speculate | | +| MOBILE_APP | 1 | 1 | apkpure | google_playstore | +| OPEN_TCP_PORT | 6 | 6 | asset_inventory, fingerprintx, httpx, nmap_xml, portfilter, sslcert | asset_inventory, censys_ip, portscan, shodan_enterprise, shodan_idb, speculate | +| OPEN_UDP_PORT | 0 | 2 | | censys_ip, shodan_enterprise | +| ORG_STUB | 4 | 1 | dockerhub, github_org, google_playstore, postman | speculate | +| PASSWORD | 0 | 2 | | credshed, dehashed | +| PROTOCOL | 3 | 2 | legba, medusa, nmap_xml | censys_ip, fingerprintx | +| RAW_DNS_RECORD | 0 | 3 | | dnsbimi, dnsresolve, dnstlsrpt | +| RAW_TEXT | 2 | 1 | excavate, trufflehog | kreuzberg | +| SOCIAL | 7 | 4 | dockerhub, github_org, gitlab_com, gitlab_onprem, gowitness, postman, speculate | dockerhub, github_usersearch, gitlab_onprem, social | +| STORAGE_BUCKET | 8 | 5 | baddns_direct, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_microsoft, speculate | bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_microsoft | +| TECHNOLOGY | 5 | 10 | asset_inventory, gitlab_onprem, trajan, web_report, wpscan | ajaxpro, badsecrets, censys_ip, dotnetnuke, gitlab_onprem, gowitness, nuclei, shodan_enterprise, shodan_idb, wpscan | +| URL | 22 | 2 | ajaxpro, aspnet_bin_exposure, asset_inventory, baddns_direct, bypass403, ffuf, git, gowitness, graphql_introspection, httpx, iis_shortnames, lightfuzz, ntlm, nuclei, portfilter, robots, smuggler, speculate, telerik, url_manipulation, wafw00f, web_report | gowitness, httpx | +| URL_HINT | 1 | 1 | ffuf_shortnames | iis_shortnames | +| URL_UNVERIFIED | 9 | 19 | code_repository, filedownload, httpx, oauth, portfilter, retirejs, social, speculate, trajan | azure_tenant, bevigil, bucket_file_enum, censys_ip, dnsbimi, dnscaa, dnstlsrpt, dockerhub, excavate, ffuf, ffuf_shortnames, github_codesearch, gowitness, hunterio, robots, securitytxt, urlscan, wayback, wpscan | +| USERNAME | 1 | 2 | speculate | credshed, dehashed | +| VHOST | 1 | 0 | web_report | | +| WAF | 1 | 1 | asset_inventory | wafw00f | +| WEBSCREENSHOT | 0 | 1 | | gowitness | +| WEB_PARAMETER | 7 | 4 | hunt, lightfuzz, paramminer_cookies, paramminer_getparams, paramminer_headers, reflected_parameters, web_parameters | excavate, paramminer_cookies, paramminer_getparams, paramminer_headers | <!-- END BBOT EVENTS --> -## Findings Vs. Vulnerabilities +## Findings -BBOT has a sharp distinction between Findings and Vulnerabilities: +All vulnerability discoveries, security-relevant observations, and other notable results in BBOT are emitted as **`FINDING`** events. -**VULNERABILITY** +Each finding has a **severity** and a **confidence**: -* There's a higher standard for what is allowed to be a vulnerability. They should be considered **confirmed** and **actionable** - no additional confirmation required -* They are always assigned a severity. The possible severities are: LOW, MEDIUM, HIGH, or CRITICAL +* **Severity** indicates impact: `INFO`, `LOW`, `MEDIUM`, `HIGH`, or `CRITICAL` +* **Confidence** indicates how certain the finding is: `CONFIRMED`, `HIGH`, `MEDIUM`, `LOW`, or `UNKNOWN` -**FINDING** - -* Findings can range anywhere from "slightly interesting behavior" to "likely, but unconfirmed vulnerability" -* Are often false positives - -By making this separation, actionable vulnerabilities can be identified quickly in the midst of a large scan +Together, these let you quickly prioritize results -- e.g. a `CRITICAL` severity with `CONFIRMED` confidence is immediately actionable, while a `MEDIUM` severity with `LOW` confidence may need manual verification. diff --git a/docs/scanning/index.md b/docs/scanning/index.md index 63ff4cda95..70ee5d179c 100644 --- a/docs/scanning/index.md +++ b/docs/scanning/index.md @@ -88,54 +88,54 @@ bbot -f subdomain-enum -l Modules can be easily enabled/disabled based on their flags: - `-f` Enable these flags (e.g. `-f subdomain-enum`) -- `-rf` Require modules to have this flag (e.g. `-rf safe`) +- `-rf` Require modules to have this flag (e.g. `-rf passive`) - `-ef` Exclude these flags (e.g. `-ef slow`) - `-em` Exclude these individual modules (e.g. `-em ipneighbor`) - `-lf` List all available flags -Every module is either `safe` or `aggressive`, and either `active` or `passive`. These can be useful for filtering. For example, if you wanted to enable all the `safe` modules, but exclude active ones, you could do: +Every module is either `active` or `passive`. Some modules are additionally tagged `loud` (generates lots of traffic) or `invasive` (intrusive or potentially destructive). These can be useful for filtering. For example, if you wanted to enable subdomain enumeration modules but exclude loud ones, you could do: ```bash -# Enable safe modules but exclude active ones -bbot -t evilcorp.com -f safe -ef active +# Enable subdomain-enum modules but exclude loud ones +bbot -t evilcorp.com -f subdomain-enum -ef loud ``` This is equivalent to requiring the passive flag: ```bash -# Enable safe modules but only if they're also passive -bbot -t evilcorp.com -f safe -rf passive +# Enable subdomain-enum modules but only if they're also passive +bbot -t evilcorp.com -f subdomain-enum -rf passive ``` -A single module can have multiple flags. For example, the `securitytrails` module is `passive`, `safe`, `subdomain-enum`. Below is a full list of flags and their associated modules. +A single module can have multiple flags. For example, the `securitytrails` module is `passive`, `subdomain-enum`. Below is a full list of flags and their associated modules. ### List of Flags <!-- BBOT MODULE FLAGS --> -| Flag | # Modules | Description | Modules | -|------------------|-------------|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| safe | 98 | Non-intrusive, safe to run | affiliates, aggregate, ajaxpro, anubisdb, apkpure, asn, aspnet_bin_exposure, azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bevigil, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, code_repository, credshed, crt, crt_db, dehashed, digitorus, dnsbimi, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, extractous, filedownload, fingerprintx, fullhunt, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, gowitness, graphql_introspection, hackertarget, httpx, hunt, hunterio, iis_shortnames, ip2location, ipstack, jadx, leakix, myssl, newsletters, ntlm, oauth, otx, passivetotal, pgp, portfilter, portscan, postman, postman_download, rapiddns, reflected_parameters, retirejs, robots, securitytrails, securitytxt, shodan_dns, shodan_idb, sitedossier, skymem, social, sslcert, subdomaincenter, subdomainradar, trickest, trufflehog, unarchive, urlscan, viewdns, virustotal, wayback | -| passive | 70 | Never connects to target systems | affiliates, aggregate, anubisdb, apkpure, asn, azure_realm, azure_tenant, bevigil, bucket_file_enum, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, code_repository, credshed, crt, crt_db, dehashed, digitorus, dnsbimi, dnscaa, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, excavate, extractous, fullhunt, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, google_playstore, hackertarget, hunterio, ip2location, ipneighbor, ipstack, jadx, leakix, myssl, otx, passivetotal, pgp, portfilter, postman, postman_download, rapiddns, securitytrails, shodan_dns, shodan_idb, sitedossier, skymem, social, speculate, subdomaincenter, subdomainradar, trickest, trufflehog, unarchive, urlscan, viewdns, virustotal, wayback | -| active | 52 | Makes active connections to target systems | ajaxpro, aspnet_bin_exposure, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_microsoft, bypass403, dnsbrute, dnsbrute_mutations, dnscommonsrv, dotnetnuke, ffuf, ffuf_shortnames, filedownload, fingerprintx, generic_ssrf, git, gitlab_com, gitlab_onprem, gowitness, graphql_introspection, host_header, httpx, hunt, iis_shortnames, legba, lightfuzz, medusa, newsletters, ntlm, nuclei, oauth, paramminer_cookies, paramminer_getparams, paramminer_headers, portscan, reflected_parameters, retirejs, robots, securitytxt, smuggler, sslcert, telerik, url_manipulation, vhost, wafw00f, wpscan | -| subdomain-enum | 51 | Enumerates subdomains | anubisdb, asn, azure_realm, azure_tenant, baddns_direct, baddns_zone, bevigil, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, crt, crt_db, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, shodan_idb, sitedossier, sslcert, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, virustotal, wayback | -| aggressive | 22 | Generates a large amount of network traffic | bypass403, dnsbrute, dnsbrute_mutations, dotnetnuke, ffuf, ffuf_shortnames, generic_ssrf, host_header, ipneighbor, legba, lightfuzz, medusa, nuclei, paramminer_cookies, paramminer_getparams, paramminer_headers, smuggler, telerik, url_manipulation, vhost, wafw00f, wpscan | -| code-enum | 18 | Find public code repositories and search them for secrets etc. | apkpure, code_repository, docker_pull, dockerhub, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, jadx, postman, postman_download, trufflehog | -| web-basic | 17 | Basic, non-intrusive web scan functionality | azure_realm, baddns, badsecrets, bucket_amazon, bucket_firebase, bucket_google, bucket_microsoft, filedownload, git, graphql_introspection, httpx, iis_shortnames, ntlm, oauth, robots, securitytxt, sslcert | -| cloud-enum | 16 | Enumerates cloud resources | azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_microsoft, dnsbimi, dnstlsrpt, httpx, oauth, securitytxt | -| web-thorough | 15 | More advanced web scanning functionality | ajaxpro, aspnet_bin_exposure, bucket_digitalocean, bypass403, dotnetnuke, ffuf_shortnames, generic_ssrf, host_header, hunt, lightfuzz, reflected_parameters, retirejs, smuggler, telerik, url_manipulation | -| slow | 11 | May take a long time to complete | bucket_digitalocean, dnsbrute_mutations, docker_pull, fingerprintx, git_clone, gitdumper, paramminer_cookies, paramminer_getparams, paramminer_headers, smuggler, vhost | -| email-enum | 9 | Enumerates email addresses | dehashed, dnscaa, dnstlsrpt, emailformat, emails, hunterio, pgp, skymem, sslcert | -| affiliates | 8 | Discovers affiliated hostnames/domains | affiliates, azure_realm, azure_tenant, builtwith, oauth, sslcert, trickest, viewdns | -| download | 7 | Modules that download files, apps, or repositories | apkpure, docker_pull, filedownload, git_clone, gitdumper, github_workflows, postman_download | -| deadly | 6 | Highly aggressive | ffuf, legba, lightfuzz, medusa, nuclei, vhost | -| baddns | 3 | Runs all modules from the DNS auditing tool BadDNS | baddns, baddns_direct, baddns_zone | -| web-paramminer | 3 | Discovers HTTP parameters through brute-force | paramminer_cookies, paramminer_getparams, paramminer_headers | -| iis-shortnames | 2 | Scans for IIS Shortname vulnerability | ffuf_shortnames, iis_shortnames | -| portscan | 2 | Discovers open ports | portscan, shodan_idb | -| social-enum | 2 | Enumerates social media | httpx, social | -| service-enum | 1 | Identifies protocols running on open ports | fingerprintx | -| subdomain-hijack | 1 | Detects hijackable subdomains | baddns | -| web-screenshots | 1 | Takes screenshots of web pages | gowitness | +| Flag | # Modules | Description | Modules | +|------------------|-------------|----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| safe | 101 | Non-intrusive and non-destructive | affiliates, aggregate, ajaxpro, anubisdb, apkpure, asn, aspnet_bin_exposure, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bevigil, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, code_repository, credshed, crt, crt_db, dehashed, digitorus, dnsbimi, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, emails, excavate, filedownload, fingerprintx, fullhunt, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, gowitness, graphql_introspection, hackertarget, httpx, hunt, hunterio, ip2location, ipstack, jadx, kreuzberg, leakix, myssl, newsletters, ntlm, oauth, otx, passivetotal, pgp, portfilter, postman, postman_download, rapiddns, reflected_parameters, retirejs, robots, securitytrails, securitytxt, shodan_dns, shodan_enterprise, shodan_idb, sitedossier, skymem, social, speculate, sslcert, subdomaincenter, subdomainradar, subdomains, trajan, trickest, trufflehog, unarchive, urlscan, viewdns, virustotal, wayback | +| passive | 71 | Never connects to target systems | affiliates, aggregate, anubisdb, apkpure, asn, azure_tenant, bevigil, bucket_file_enum, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, code_repository, credshed, crt, crt_db, dehashed, digitorus, dnsbimi, dnscaa, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, excavate, fullhunt, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, google_playstore, hackertarget, hunterio, ip2location, ipneighbor, ipstack, jadx, kreuzberg, leakix, myssl, otx, passivetotal, pgp, portfilter, postman, postman_download, rapiddns, securitytrails, shodan_dns, shodan_enterprise, shodan_idb, sitedossier, skymem, social, speculate, subdomaincenter, subdomainradar, trajan, trickest, trufflehog, unarchive, urlscan, viewdns, virustotal, wayback | +| active | 50 | Makes active connections to target systems | ajaxpro, aspnet_bin_exposure, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_microsoft, bypass403, dnsbrute, dnsbrute_mutations, dnscommonsrv, dotnetnuke, ffuf, ffuf_shortnames, filedownload, fingerprintx, git, gitlab_com, gitlab_onprem, gowitness, graphql_introspection, host_header, httpx, hunt, iis_shortnames, legba, lightfuzz, medusa, newsletters, ntlm, nuclei, oauth, paramminer_cookies, paramminer_getparams, paramminer_headers, portscan, reflected_parameters, retirejs, robots, securitytxt, smuggler, sslcert, telerik, url_manipulation, wafw00f, wpscan | +| subdomain-enum | 50 | Enumerates subdomains | anubisdb, asn, azure_tenant, baddns_direct, baddns_zone, bevigil, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, crt, crt_db, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, shodan_idb, sitedossier, sslcert, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, virustotal, wayback | +| loud | 22 | Generates a large amount of network traffic | bypass403, dnsbrute, dnsbrute_mutations, dotnetnuke, ffuf, ffuf_shortnames, host_header, iis_shortnames, ipneighbor, legba, lightfuzz, medusa, nuclei, paramminer_cookies, paramminer_getparams, paramminer_headers, portscan, smuggler, telerik, url_manipulation, wafw00f, wpscan | +| code-enum | 19 | Find public code repositories and search them for secrets etc. | apkpure, code_repository, docker_pull, dockerhub, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, jadx, postman, postman_download, trajan, trufflehog | +| web | 16 | Non-intrusive web scan functionality | baddns, badsecrets, bucket_amazon, bucket_firebase, bucket_google, bucket_microsoft, filedownload, git, graphql_introspection, httpx, iis_shortnames, ntlm, oauth, robots, securitytxt, sslcert | +| cloud-enum | 15 | Enumerates cloud resources | azure_tenant, baddns, baddns_direct, baddns_zone, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_microsoft, dnsbimi, dnstlsrpt, httpx, oauth, securitytxt | +| web-heavy | 14 | More advanced web scanning functionality | ajaxpro, aspnet_bin_exposure, bucket_digitalocean, bypass403, dotnetnuke, ffuf_shortnames, host_header, hunt, lightfuzz, reflected_parameters, retirejs, smuggler, telerik, url_manipulation | +| slow | 10 | May take a long time to complete | bucket_digitalocean, dnsbrute_mutations, docker_pull, fingerprintx, git_clone, gitdumper, paramminer_cookies, paramminer_getparams, paramminer_headers, smuggler | +| email-enum | 9 | Enumerates email addresses | dehashed, dnscaa, dnstlsrpt, emailformat, emails, hunterio, pgp, skymem, sslcert | +| affiliates | 7 | Discovers affiliated hostnames/domains | affiliates, azure_tenant, builtwith, oauth, sslcert, trickest, viewdns | +| download | 7 | Modules that download files, apps, or repositories | apkpure, docker_pull, filedownload, git_clone, gitdumper, github_workflows, postman_download | +| invasive | 7 | Intrusive or potentially destructive | dotnetnuke, legba, lightfuzz, medusa, nuclei, smuggler, telerik | +| baddns | 3 | Runs all modules from the DNS auditing tool BadDNS | baddns, baddns_direct, baddns_zone | +| web-paramminer | 3 | Discovers HTTP parameters through brute-force | paramminer_cookies, paramminer_getparams, paramminer_headers | +| iis-shortnames | 2 | Scans for IIS Shortname vulnerability | ffuf_shortnames, iis_shortnames | +| portscan | 2 | Discovers open ports | portscan, shodan_idb | +| social-enum | 2 | Enumerates social media | httpx, social | +| service-enum | 1 | Identifies protocols running on open ports | fingerprintx | +| subdomain-hijack | 1 | Detects hijackable subdomains | baddns | +| web-screenshots | 1 | Takes screenshots of web pages | gowitness | <!-- END BBOT MODULE FLAGS --> ## Dependencies @@ -177,28 +177,32 @@ bbot -t evilcorp.com -f subdomain-enum -c scope.report_distance=1 If you want to scan **_only_** that specific target hostname and none of its children, you can specify `--strict-scope`. -Note that `--strict-scope` only applies to targets and whitelists, but not blacklists. This means that if you put `internal.evilcorp.com` in your blacklist, you can be sure none of its subdomains will be scanned, even when using `--strict-scope`. +Note that `--strict-scope` only applies to targets, but not blacklists. This means that if you put `internal.evilcorp.com` in your blacklist, you can be sure none of its subdomains will be scanned, even when using `--strict-scope`. -### Whitelists and Blacklists +### Targets, Seeds, and Blacklists -BBOT allows precise control over scope with whitelists and blacklists. These both use the same syntax as `--target`, meaning they accept the same event types, and you can specify an unlimited number of them, via a file, the CLI, or both. +BBOT uses three related concepts to control scope and how a scan is driven: -#### Whitelists +- **Targets (`-t` / `--targets`)**: Define what is in-scope. These also act as scan seeds if seeds aren't explicitly defined. +- **Seeds (`-s` / `--seeds`)**: Seeds define the starting point for the scan. They drive **passive** modules and can be outside of the explicit target list (out of scope) for those passive modules. If you don’t specify `--seeds`, BBOT will automatically use your targets as seeds. +- **Blacklists (`-b` / `--blacklist`)**: Define what is **never** touched. Anything matching the blacklist is excluded from the scan, even if it would otherwise be in-scope. -`--whitelist` enables you to override what's in scope. For example, if you want to run nuclei against `evilcorp.com`, but stay only inside their corporate IP range of `1.2.3.0/24`, you can accomplish this like so: +This separation lets you, for example, keep a tight target list for what’s considered in-scope, while still allowing passive modules to discover new subdomains that may ultimately be in-scope. The blacklist helps to mask-off anything that you know should not be scanned. + +For example, lets say you have a target with subdomains that resolve both within, and outside of an IP range that defines your scope. You can set the IP range as the **target**, and then safely let BBOT explore the domain defined in **seeds**. Any discovered assets that are in your scope will automatically be assessed by active modules. ```bash -# Seed scan with evilcorp.com, but restrict scope to 1.2.3.0/24 -bbot -t evilcorp.com --whitelist 1.2.3.0/24 -f subdomain-enum -m portscan nuclei --allow-deadly +bbot -t 192.168.1.0/24 -s evilcorp.com -f subdomain-enum -m nuclei ``` +In this example, any discovered `evilcorp.com` subdomains that resolve within `192.168.1.0/24` will be scanned by `Nuclei`. Any others will be discovered, but not touched by active modules. #### Blacklists -`--blacklist` takes ultimate precedence. Anything in the blacklist is completely excluded from the scan, even if it's in the whitelist. +`--blacklist` takes ultimate precedence. Anything in the blacklist is completely excluded from the scan, even if it would otherwise be in-scope based on your targets or seeds. ```bash # Scan evilcorp.com, but exclude internal.evilcorp.com and its children -bbot -t evilcorp.com --blacklist internal.evilcorp.com -f subdomain-enum -m portscan nuclei --allow-deadly +bbot -t evilcorp.com --blacklist internal.evilcorp.com -f subdomain-enum -m portscan nuclei ``` #### Blacklist by Regex @@ -222,7 +226,7 @@ If you only want to blacklist the URL, you could narrow the regex like so: bbot -t evilcorp.com --blacklist 'RE:signout\.aspx$' ``` -Similar to targets and whitelists, blacklists can be specified in your preset. The `spider` preset makes use of this to prevent the spider from following logout links: +Similar to targets, blacklists can be specified in your preset. The `spider` preset makes use of this to prevent the spider from following logout links: ```yaml title="spider.yml" description: Recursive web spider @@ -277,4 +281,4 @@ dns: wildcard_tests: 20 ``` -If that doesn't work you can consider [blacklisting](#whitelists-and-blacklists) the offending domain. +If that doesn't work you can consider [blacklisting](#targets-seeds-and-blacklists) the offending domain. diff --git a/docs/scanning/output.md b/docs/scanning/output.md index b46eb40c86..3fa69f131b 100644 --- a/docs/scanning/output.md +++ b/docs/scanning/output.md @@ -102,24 +102,23 @@ config: bbot -t evilcorp.com -om discord -c modules.discord.webhook_url=https://discord.com/api/webhooks/1234/deadbeef ``` -By default, only `VULNERABILITY` and `FINDING` events are sent, but this can be customized by setting `event_types` in the config like so: +By default, only `FINDING` events are sent, but this can be customized by setting `event_types` in the config like so: ```yaml title="discord_preset.yml" config: modules: discord: event_types: - - VULNERABILITY - FINDING - STORAGE_BUCKET ``` ...or on the command line: ```bash -bbot -t evilcorp.com -om discord -c modules.discord.event_types=["STORAGE_BUCKET","FINDING","VULNERABILITY"] +bbot -t evilcorp.com -om discord -c modules.discord.event_types=["STORAGE_BUCKET","FINDING"] ``` -You can also filter on the severity of `VULNERABILITY` events by setting `min_severity`: +You can also filter on the severity of `FINDING` events by setting `min_severity`: ```yaml title="discord_preset.yml" @@ -155,15 +154,20 @@ config: ### Elasticsearch -When outputting to Elastic, use the `http` output module with the following settings (replace `<your_index>` with your desired index, e.g. `bbot`): +- Step 1: Spin up a quick Elasticsearch docker image + +```bash +docker run -d -p 9200:9200 --name=bbot-elastic --v "$(pwd)/elastic_data:/usr/share/elasticsearch/data" -e ELASTIC_PASSWORD=bbotislife -m 1GB docker.elastic.co/elasticsearch/elasticsearch:8.16.0 +``` + +- Step 2: Execute a scan with `elastic` output module ```bash # send scan results directly to elasticsearch -bbot -t evilcorp.com -om http -c \ - modules.http.url=http://localhost:8000/<your_index>/_doc \ - modules.http.siem_friendly=true \ - modules.http.username=elastic \ - modules.http.password=changeme +# note: you can replace "bbot" with your own index name +bbot -t evilcorp.com -om elastic -c \ + modules.elastic.url=https://localhost:9200/bbot/_doc \ + modules.elastic.password=bbotislife ``` Alternatively, via a preset: @@ -171,11 +175,9 @@ Alternatively, via a preset: ```yaml title="elastic_preset.yml" config: modules: - http: - url: http://localhost:8000/<your_index>/_doc - siem_friendly: true - username: elastic - password: changeme + elastic: + url: http://localhost:9200/bbot/_doc + password: bbotislife ``` ### Splunk diff --git a/docs/scanning/presets.md b/docs/scanning/presets.md index 7fa8f8c93b..fc5e9426f8 100644 --- a/docs/scanning/presets.md +++ b/docs/scanning/presets.md @@ -61,7 +61,7 @@ target: include: # include these default presets - subdomain-enum - - web-basic + - web modules: # enable nuclei in addition to the other modules @@ -122,7 +122,27 @@ bbot -p ./mypreset.yml --current-preset ## Advanced Usage -BBOT Presets support advanced features like environment variable substitution and custom conditions. +BBOT Presets support advanced features like file-based targets, environment variable substitution, and custom conditions. + +### Files as Targets + +You can specify file paths in your preset's `target`, `seeds`, or `blacklist` fields. BBOT will read each file and expand its lines as individual entries: + +```yaml title="my_preset.yml" +target: + - targets.txt + - extra.evilcorp.com + +seeds: + - seeds.txt + +blacklist: + - /home/user/blacklist.txt +``` + +Relative paths (like `targets.txt`) are resolved relative to the preset file's directory first, then the current working directory. Absolute paths are used as-is. + +You can mix file paths and literal targets in the same list. If an entry doesn't point to an existing file, it is treated as a literal target. ### Custom Modules diff --git a/docs/scanning/presets_list.md b/docs/scanning/presets_list.md index f07d4cda45..98a4d126ec 100644 --- a/docs/scanning/presets_list.md +++ b/docs/scanning/presets_list.md @@ -1,29 +1,61 @@ Below is a list of every default BBOT preset, including its YAML. <!-- BBOT PRESET YAML --> -## **baddns-intense** +## **baddns** + +Check for subdomain takeovers and other DNS issues. + +??? note "`baddns.yml`" + ```yaml title="~/.bbot/presets/baddns.yml" + description: Check for subdomain takeovers and other DNS issues. + + modules: + - baddns + + config: + modules: + baddns: + enabled_submodules: [CNAME, MX, TXT] + min_severity: LOW + min_confidence: MODERATE + ``` + + + +Modules: [0]("") + +## **baddns-heavy** Run all baddns modules and submodules. -??? note "`baddns-intense.yml`" - ```yaml title="~/.bbot/presets/baddns-intense.yml" +??? note "`baddns-heavy.yml`" + ```yaml title="~/.bbot/presets/baddns-heavy.yml" description: Run all baddns modules and submodules. + include: + - baddns modules: - - baddns - baddns_zone - baddns_direct config: modules: baddns: - enabled_submodules: [CNAME,references,MX,NS,TXT] + enabled_submodules: [CNAME, NS, MX, TXT, references, DMARC, SPF, MTA-STS, WILDCARD] + min_severity: INFORMATIONAL + min_confidence: UNKNOWN + baddns_zone: + min_severity: INFORMATIONAL + min_confidence: UNKNOWN + baddns_direct: + min_severity: INFORMATIONAL + min_confidence: UNKNOWN ``` -Modules: [4]("`baddns_direct`, `baddns_zone`, `baddns`, `httpx`") +Modules: [0]("") ## **cloud-enum** @@ -42,7 +74,7 @@ Enumerate cloud resources such as storage buckets, etc. -Modules: [58]("`anubisdb`, `asn`, `azure_realm`, `azure_tenant`, `baddns_direct`, `baddns_zone`, `baddns`, `bevigil`, `bucket_amazon`, `bucket_digitalocean`, `bucket_file_enum`, `bucket_firebase`, `bucket_google`, `bucket_microsoft`, `bufferoverrun`, `builtwith`, `c99`, `censys_dns`, `certspotter`, `chaos`, `crt_db`, `crt`, `digitorus`, `dnsbimi`, `dnsbrute_mutations`, `dnsbrute`, `dnscaa`, `dnscommonsrv`, `dnsdumpster`, `dnstlsrpt`, `fullhunt`, `github_codesearch`, `github_org`, `hackertarget`, `httpx`, `hunterio`, `ipneighbor`, `leakix`, `myssl`, `oauth`, `otx`, `passivetotal`, `postman_download`, `postman`, `rapiddns`, `securitytrails`, `securitytxt`, `shodan_dns`, `shodan_idb`, `sitedossier`, `social`, `sslcert`, `subdomaincenter`, `subdomainradar`, `trickest`, `urlscan`, `virustotal`, `wayback`") +Modules: [0]("") ## **code-enum** @@ -58,7 +90,7 @@ Enumerate Git repositories, Docker images, etc. -Modules: [20]("`apkpure`, `code_repository`, `docker_pull`, `dockerhub`, `git_clone`, `git`, `gitdumper`, `github_codesearch`, `github_org`, `github_usersearch`, `github_workflows`, `gitlab_com`, `gitlab_onprem`, `google_playstore`, `httpx`, `jadx`, `postman_download`, `postman`, `social`, `trufflehog`") +Modules: [0]("") ## **dirbust-heavy** @@ -109,7 +141,7 @@ Recursive web directory brute-force (aggressive) Category: web -Modules: [5]("`ffuf_shortnames`, `ffuf`, `httpx`, `iis_shortnames`, `wayback`") +Modules: [0]("") ## **dirbust-light** @@ -134,7 +166,7 @@ Basic web directory brute-force (surface-level directories only) Category: web -Modules: [4]("`ffuf_shortnames`, `ffuf`, `httpx`, `iis_shortnames`") +Modules: [0]("") ## **dotnet-audit** @@ -172,7 +204,7 @@ Comprehensive scan for all IIS/.NET specific modules and module settings Category: web -Modules: [9]("`ajaxpro`, `aspnet_bin_exposure`, `badsecrets`, `dotnetnuke`, `ffuf_shortnames`, `ffuf`, `httpx`, `iis_shortnames`, `telerik`") +Modules: [0]("") ## **email-enum** @@ -191,7 +223,7 @@ Enumerate email addresses from APIs, web crawling, etc. -Modules: [8]("`dehashed`, `dnscaa`, `dnstlsrpt`, `emailformat`, `hunterio`, `pgp`, `skymem`, `sslcert`") +Modules: [0]("") ## **fast** @@ -241,7 +273,7 @@ Recursively enumerate IIS shortnames Category: web -Modules: [3]("`ffuf_shortnames`, `httpx`, `iis_shortnames`") +Modules: [0]("") ## **kitchen-sink** @@ -257,32 +289,54 @@ Everything everywhere all at once - code-enum - email-enum - spider - - web-basic + - web - paramminer - dirbust-light - web-screenshots - - baddns-intense + - baddns-heavy + ``` + + + +Modules: [0]("") + +## **lightfuzz** + +Default fuzzing: all 9 submodules (cmdi, crypto, path, serial, sqli, ssti, xss, esi, ssrf) plus companion modules (badsecrets, hunt, reflected_parameters). POST fuzzing disabled but try_post_as_get enabled, so POST params are retested as GET. Skips confirmed WAFs. + +??? note "`lightfuzz.yml`" + ```yaml title="~/.bbot/presets/web/lightfuzz.yml" + description: "Default fuzzing: all 9 submodules (cmdi, crypto, path, serial, sqli, ssti, xss, esi, ssrf) plus companion modules (badsecrets, hunt, reflected_parameters). POST fuzzing disabled but try_post_as_get enabled, so POST params are retested as GET. Skips confirmed WAFs." + + include: + - lightfuzz-light + modules: + - badsecrets + - hunt + - reflected_parameters + config: modules: - baddns: - enable_references: True + lightfuzz: + enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] + try_post_as_get: True ``` +Category: web - -Modules: [90]("`anubisdb`, `apkpure`, `asn`, `azure_realm`, `azure_tenant`, `baddns_direct`, `baddns_zone`, `baddns`, `badsecrets`, `bevigil`, `bucket_amazon`, `bucket_digitalocean`, `bucket_file_enum`, `bucket_firebase`, `bucket_google`, `bucket_microsoft`, `bufferoverrun`, `builtwith`, `c99`, `censys_dns`, `certspotter`, `chaos`, `code_repository`, `crt_db`, `crt`, `dehashed`, `digitorus`, `dnsbimi`, `dnsbrute_mutations`, `dnsbrute`, `dnscaa`, `dnscommonsrv`, `dnsdumpster`, `dnstlsrpt`, `docker_pull`, `dockerhub`, `emailformat`, `ffuf_shortnames`, `ffuf`, `filedownload`, `fullhunt`, `git_clone`, `git`, `gitdumper`, `github_codesearch`, `github_org`, `github_usersearch`, `github_workflows`, `gitlab_com`, `gitlab_onprem`, `google_playstore`, `gowitness`, `graphql_introspection`, `hackertarget`, `httpx`, `hunt`, `hunterio`, `iis_shortnames`, `ipneighbor`, `jadx`, `leakix`, `myssl`, `ntlm`, `oauth`, `otx`, `paramminer_cookies`, `paramminer_getparams`, `paramminer_headers`, `passivetotal`, `pgp`, `postman_download`, `postman`, `rapiddns`, `reflected_parameters`, `robots`, `securitytrails`, `securitytxt`, `shodan_dns`, `shodan_idb`, `sitedossier`, `skymem`, `social`, `sslcert`, `subdomaincenter`, `subdomainradar`, `trickest`, `trufflehog`, `urlscan`, `virustotal`, `wayback`") +Modules: [0]("") ## **lightfuzz-heavy** -Discover web parameters and lightly fuzz them for vulnerabilities, with more intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, and adds paramminer modules for parameter discovery. Avoids running against confirmed WAFs. +Aggressive fuzzing: everything in lightfuzz, plus paramminer brute-force parameter discovery (headers, GET params, cookies), POST request fuzzing enabled, try_get_as_post enabled (GET params retested as POST), and robots.txt parsing. Still skips confirmed WAFs. ??? note "`lightfuzz-heavy.yml`" ```yaml title="~/.bbot/presets/web/lightfuzz-heavy.yml" - description: Discover web parameters and lightly fuzz them for vulnerabilities, with more intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, and adds paramminer modules for parameter discovery. Avoids running against confirmed WAFs. + description: "Aggressive fuzzing: everything in lightfuzz, plus paramminer brute-force parameter discovery (headers, GET params, cookies), POST request fuzzing enabled, try_get_as_post enabled (GET params retested as POST), and robots.txt parsing. Still skips confirmed WAFs." include: - - lightfuzz-medium + - lightfuzz flags: - web-paramminer @@ -293,7 +347,7 @@ Discover web parameters and lightly fuzz them for vulnerabilities, with more int config: modules: lightfuzz: - enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi] + enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] disable_post: False try_post_as_get: True try_get_as_post: True @@ -301,15 +355,15 @@ Discover web parameters and lightly fuzz them for vulnerabilities, with more int Category: web -Modules: [10]("`badsecrets`, `httpx`, `hunt`, `lightfuzz`, `paramminer_cookies`, `paramminer_getparams`, `paramminer_headers`, `portfilter`, `reflected_parameters`, `robots`") +Modules: [0]("") ## **lightfuzz-light** -Discover web parameters and lightly fuzz them for vulnerabilities, with only the most common vulnerabilities and minimal extra modules. Safest to run alongside larger scans. +Minimal fuzzing: only path traversal, SQLi, and XSS submodules. No POST requests. No companion modules. Safest option for running alongside larger scans with minimal overhead. ??? note "`lightfuzz-light.yml`" ```yaml title="~/.bbot/presets/web/lightfuzz-light.yml" - description: Discover web parameters and lightly fuzz them for vulnerabilities, with only the most common vulnerabilities and minimal extra modules. Safest to run alongside larger scans. + description: "Minimal fuzzing: only path traversal, SQLi, and XSS submodules. No POST requests. No companion modules. Safest option for running alongside larger scans with minimal overhead." modules: - httpx @@ -328,48 +382,21 @@ Discover web parameters and lightly fuzz them for vulnerabilities, with only the conditions: - | {% if config.web.spider_distance == 0 %} - {{ warn("Lightfuzz works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }} + {{ warn("Lightfuzz works much better with spider enabled! Consider adding 'spider' or 'spider-heavy' preset.") }} {% endif %} ``` Category: web -Modules: [3]("`httpx`, `lightfuzz`, `portfilter`") - -## **lightfuzz-medium** - -Discover web parameters and lightly fuzz them for vulnerabilities. Uses all lightfuzz modules, without some of the more intense discovery techniques. Does not send POST requests. This is the default lightfuzz preset; if you're not sure which one to use, this is a good starting point. Avoids running against confirmed WAFs. - -??? note "`lightfuzz-medium.yml`" - ```yaml title="~/.bbot/presets/web/lightfuzz-medium.yml" - description: Discover web parameters and lightly fuzz them for vulnerabilities. Uses all lightfuzz modules, without some of the more intense discovery techniques. Does not send POST requests. This is the default lightfuzz preset; if you're not sure which one to use, this is a good starting point. Avoids running against confirmed WAFs. - - include: - - lightfuzz-light - - modules: - - badsecrets - - hunt - - reflected_parameters - - config: - modules: - lightfuzz: - enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi] - try_post_as_get: True - ``` - -Category: web - -Modules: [6]("`badsecrets`, `httpx`, `hunt`, `lightfuzz`, `portfilter`, `reflected_parameters`") +Modules: [0]("") -## **lightfuzz-superheavy** +## **lightfuzz-max** -Discover web parameters and lightly fuzz them for vulnerabilities, with the most intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, adds paramminer modules for parameter discovery, and tests each unique parameter-value instance individually. +Maximum fuzzing: everything in lightfuzz-heavy, plus WAF targets are no longer skipped, each unique parameter-value pair is fuzzed individually (no collapsing), common headers like X-Forwarded-For are fuzzed even if not observed, and potential parameters are speculated from JSON/XML response bodies. Significantly increases scan time. -??? note "`lightfuzz-superheavy.yml`" - ```yaml title="~/.bbot/presets/web/lightfuzz-superheavy.yml" - description: Discover web parameters and lightly fuzz them for vulnerabilities, with the most intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, adds paramminer modules for parameter discovery, and tests each unique parameter-value instance individually. +??? note "`lightfuzz-max.yml`" + ```yaml title="~/.bbot/presets/web/lightfuzz-max.yml" + description: "Maximum fuzzing: everything in lightfuzz-heavy, plus WAF targets are no longer skipped, each unique parameter-value pair is fuzzed individually (no collapsing), common headers like X-Forwarded-For are fuzzed even if not observed, and potential parameters are speculated from JSON/XML response bodies. Significantly increases scan time." include: - lightfuzz-heavy @@ -379,7 +406,7 @@ Discover web parameters and lightly fuzz them for vulnerabilities, with the most modules: lightfuzz: force_common_headers: True # Fuzz common headers like X-Forwarded-For even if they're not observed on the target - enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi] + enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] avoid_wafs: False excavate: speculate_params: True # speculate potential parameters extracted from JSON/XML web responses @@ -387,15 +414,15 @@ Discover web parameters and lightly fuzz them for vulnerabilities, with the most Category: web -Modules: [10]("`badsecrets`, `httpx`, `hunt`, `lightfuzz`, `paramminer_cookies`, `paramminer_getparams`, `paramminer_headers`, `portfilter`, `reflected_parameters`, `robots`") +Modules: [0]("") ## **lightfuzz-xss** -Discover web parameters and lightly fuzz them, limited to just GET-based xss vulnerabilities. Avoids running against confirmed WAFs. This is an example of a custom lightfuzz preset, selectively enabling a single lightfuzz module. +XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST disabled, no query string collapsing. Example of a focused single-submodule preset. ??? note "`lightfuzz-xss.yml`" ```yaml title="~/.bbot/presets/web/lightfuzz-xss.yml" - description: Discover web parameters and lightly fuzz them, limited to just GET-based xss vulnerabilities. Avoids running against confirmed WAFs. This is an example of a custom lightfuzz preset, selectively enabling a single lightfuzz module. + description: "XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST disabled, no query string collapsing. Example of a focused single-submodule preset." modules: - httpx @@ -415,13 +442,13 @@ Discover web parameters and lightly fuzz them, limited to just GET-based xss vul conditions: - | {% if config.web.spider_distance == 0 %} - {{ warn("The lightfuzz-xss preset works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }} + {{ warn("The lightfuzz-xss preset works much better with spider enabled! Consider adding 'spider' or 'spider-heavy' preset.") }} {% endif %} ``` Category: web -Modules: [5]("`httpx`, `lightfuzz`, `paramminer_getparams`, `portfilter`, `reflected_parameters`") +Modules: [0]("") ## **nuclei** @@ -467,7 +494,7 @@ Run nuclei scans against all discovered targets Category: nuclei -Modules: [3]("`httpx`, `nuclei`, `portfilter`") +Modules: [0]("") ## **nuclei-budget** @@ -498,14 +525,14 @@ Run nuclei scans against all discovered targets, using budget mode to look for l Category: nuclei -Modules: [3]("`httpx`, `nuclei`, `portfilter`") +Modules: [0]("") -## **nuclei-intense** +## **nuclei-heavy** Run nuclei scans against all discovered targets, allowing for spidering, against ALL URLs, and with additional discovery modules. -??? note "`nuclei-intense.yml`" - ```yaml title="~/.bbot/presets/nuclei/nuclei-intense.yml" +??? note "`nuclei-heavy.yml`" + ```yaml title="~/.bbot/presets/nuclei/nuclei-heavy.yml" description: Run nuclei scans against all discovered targets, allowing for spidering, against ALL URLs, and with additional discovery modules. modules: @@ -526,7 +553,7 @@ Run nuclei scans against all discovered targets, allowing for spidering, against conditions: - | {% if config.web.spider_distance == 0 and config.modules.nuclei.directory_only == False %} - {{ warn("The 'nuclei-intense' preset turns the 'directory_only' limitation off on the nuclei module. To make the best use of this, you may want to enable spidering with 'spider' or 'spider-intense' preset.") }} + {{ warn("The 'nuclei-heavy' preset turns the 'directory_only' limitation off on the nuclei module. To make the best use of this, you may want to enable spidering with 'spider' or 'spider-heavy' preset.") }} {% endif %} @@ -538,7 +565,7 @@ Run nuclei scans against all discovered targets, allowing for spidering, against Category: nuclei -Modules: [6]("`httpx`, `nuclei`, `portfilter`, `robots`, `urlscan`, `wayback`") +Modules: [0]("") ## **nuclei-technology** @@ -573,7 +600,7 @@ Run nuclei scans against all discovered targets, running templates which match d Category: nuclei -Modules: [3]("`httpx`, `nuclei`, `portfilter`") +Modules: [0]("") ## **paramminer** @@ -594,13 +621,13 @@ Discover new web parameters via brute-force, and analyze them with additional mo conditions: - | {% if config.web.spider_distance == 0 %} - {{ warn("The paramminer preset works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }} + {{ warn("The paramminer preset works much better with spider enabled! Consider adding 'spider' or 'spider-heavy' preset.") }} {% endif %} ``` Category: web -Modules: [6]("`httpx`, `hunt`, `paramminer_cookies`, `paramminer_getparams`, `paramminer_headers`, `reflected_parameters`") +Modules: [0]("") ## **spider** @@ -629,14 +656,14 @@ Recursive web spider -Modules: [1]("`httpx`") +Modules: [0]("") -## **spider-intense** +## **spider-heavy** Recursive web spider with more aggressive settings -??? note "`spider-intense.yml`" - ```yaml title="~/.bbot/presets/spider-intense.yml" +??? note "`spider-heavy.yml`" + ```yaml title="~/.bbot/presets/spider-heavy.yml" description: Recursive web spider with more aggressive settings include: @@ -654,7 +681,7 @@ Recursive web spider with more aggressive settings -Modules: [1]("`httpx`") +Modules: [0]("") ## **subdomain-enum** @@ -688,7 +715,7 @@ Enumerate subdomains via APIs, brute-force -Modules: [51]("`anubisdb`, `asn`, `azure_realm`, `azure_tenant`, `baddns_direct`, `baddns_zone`, `bevigil`, `bufferoverrun`, `builtwith`, `c99`, `censys_dns`, `certspotter`, `chaos`, `crt_db`, `crt`, `digitorus`, `dnsbimi`, `dnsbrute_mutations`, `dnsbrute`, `dnscaa`, `dnscommonsrv`, `dnsdumpster`, `dnstlsrpt`, `fullhunt`, `github_codesearch`, `github_org`, `hackertarget`, `httpx`, `hunterio`, `ipneighbor`, `leakix`, `myssl`, `oauth`, `otx`, `passivetotal`, `postman_download`, `postman`, `rapiddns`, `securitytrails`, `securitytxt`, `shodan_dns`, `shodan_idb`, `sitedossier`, `social`, `sslcert`, `subdomaincenter`, `subdomainradar`, `trickest`, `urlscan`, `virustotal`, `wayback`") +Modules: [0]("") ## **tech-detect** @@ -710,26 +737,46 @@ Detect technologies via Nuclei, and FingerprintX -Modules: [3]("`fingerprintx`, `httpx`, `nuclei`") +Modules: [0]("") -## **web-basic** +## **web** Quick web scan -??? note "`web-basic.yml`" - ```yaml title="~/.bbot/presets/web-basic.yml" +??? note "`web.yml`" + ```yaml title="~/.bbot/presets/web.yml" description: Quick web scan include: - iis-shortnames flags: - - web-basic + - web ``` -Modules: [18]("`azure_realm`, `baddns`, `badsecrets`, `bucket_amazon`, `bucket_firebase`, `bucket_google`, `bucket_microsoft`, `ffuf_shortnames`, `filedownload`, `git`, `graphql_introspection`, `httpx`, `iis_shortnames`, `ntlm`, `oauth`, `robots`, `securitytxt`, `sslcert`") +Modules: [0]("") + +## **web-heavy** + +Aggressive web scan + +??? note "`web-heavy.yml`" + ```yaml title="~/.bbot/presets/web-heavy.yml" + description: Aggressive web scan + + include: + # include the web preset + - web + + flags: + - web-heavy + ``` + + + +Modules: [0]("") ## **web-screenshots** @@ -755,27 +802,7 @@ Take screenshots of webpages -Modules: [3]("`gowitness`, `httpx`, `social`") - -## **web-thorough** - -Aggressive web scan - -??? note "`web-thorough.yml`" - ```yaml title="~/.bbot/presets/web-thorough.yml" - description: Aggressive web scan - - include: - # include the web-basic preset - - web-basic - - flags: - - web-thorough - ``` - - - -Modules: [32]("`ajaxpro`, `aspnet_bin_exposure`, `azure_realm`, `baddns`, `badsecrets`, `bucket_amazon`, `bucket_digitalocean`, `bucket_firebase`, `bucket_google`, `bucket_microsoft`, `bypass403`, `dotnetnuke`, `ffuf_shortnames`, `filedownload`, `generic_ssrf`, `git`, `graphql_introspection`, `host_header`, `httpx`, `hunt`, `iis_shortnames`, `lightfuzz`, `ntlm`, `oauth`, `reflected_parameters`, `retirejs`, `robots`, `securitytxt`, `smuggler`, `sslcert`, `telerik`, `url_manipulation`") +Modules: [0]("") <!-- END BBOT PRESET YAML --> ## Table of Default Presets @@ -783,33 +810,34 @@ Modules: [32]("`ajaxpro`, `aspnet_bin_exposure`, `azure_realm`, `baddns`, `badse Here is a the same data, but in a table: <!-- BBOT PRESETS --> -| Preset | Category | Description | # Modules | Modules | -|----------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| baddns-intense | | Run all baddns modules and submodules. | 4 | baddns, baddns_direct, baddns_zone, httpx | -| cloud-enum | | Enumerate cloud resources such as storage buckets, etc. | 58 | anubisdb, asn, azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, bevigil, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, crt, crt_db, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, shodan_idb, sitedossier, social, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, virustotal, wayback | -| code-enum | | Enumerate Git repositories, Docker images, etc. | 20 | apkpure, code_repository, docker_pull, dockerhub, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, httpx, jadx, postman, postman_download, social, trufflehog | -| dirbust-heavy | web | Recursive web directory brute-force (aggressive) | 5 | ffuf, ffuf_shortnames, httpx, iis_shortnames, wayback | -| dirbust-light | web | Basic web directory brute-force (surface-level directories only) | 4 | ffuf, ffuf_shortnames, httpx, iis_shortnames | -| dotnet-audit | web | Comprehensive scan for all IIS/.NET specific modules and module settings | 9 | ajaxpro, aspnet_bin_exposure, badsecrets, dotnetnuke, ffuf, ffuf_shortnames, httpx, iis_shortnames, telerik | -| email-enum | | Enumerate email addresses from APIs, web crawling, etc. | 8 | dehashed, dnscaa, dnstlsrpt, emailformat, hunterio, pgp, skymem, sslcert | -| fast | | Scan only the provided targets as fast as possible - no extra discovery | 0 | | -| iis-shortnames | web | Recursively enumerate IIS shortnames | 3 | ffuf_shortnames, httpx, iis_shortnames | -| kitchen-sink | | Everything everywhere all at once | 90 | anubisdb, apkpure, asn, azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bevigil, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, code_repository, crt, crt_db, dehashed, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, ffuf, ffuf_shortnames, filedownload, fullhunt, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, gowitness, graphql_introspection, hackertarget, httpx, hunt, hunterio, iis_shortnames, ipneighbor, jadx, leakix, myssl, ntlm, oauth, otx, paramminer_cookies, paramminer_getparams, paramminer_headers, passivetotal, pgp, postman, postman_download, rapiddns, reflected_parameters, robots, securitytrails, securitytxt, shodan_dns, shodan_idb, sitedossier, skymem, social, sslcert, subdomaincenter, subdomainradar, trickest, trufflehog, urlscan, virustotal, wayback | -| lightfuzz-heavy | web | Discover web parameters and lightly fuzz them for vulnerabilities, with more intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, and adds paramminer modules for parameter discovery. Avoids running against confirmed WAFs. | 10 | badsecrets, httpx, hunt, lightfuzz, paramminer_cookies, paramminer_getparams, paramminer_headers, portfilter, reflected_parameters, robots | -| lightfuzz-light | web | Discover web parameters and lightly fuzz them for vulnerabilities, with only the most common vulnerabilities and minimal extra modules. Safest to run alongside larger scans. | 3 | httpx, lightfuzz, portfilter | -| lightfuzz-medium | web | Discover web parameters and lightly fuzz them for vulnerabilities. Uses all lightfuzz modules, without some of the more intense discovery techniques. Does not send POST requests. This is the default lightfuzz preset; if you're not sure which one to use, this is a good starting point. Avoids running against confirmed WAFs. | 6 | badsecrets, httpx, hunt, lightfuzz, portfilter, reflected_parameters | -| lightfuzz-superheavy | web | Discover web parameters and lightly fuzz them for vulnerabilities, with the most intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, adds paramminer modules for parameter discovery, and tests each unique parameter-value instance individually. | 10 | badsecrets, httpx, hunt, lightfuzz, paramminer_cookies, paramminer_getparams, paramminer_headers, portfilter, reflected_parameters, robots | -| lightfuzz-xss | web | Discover web parameters and lightly fuzz them, limited to just GET-based xss vulnerabilities. Avoids running against confirmed WAFs. This is an example of a custom lightfuzz preset, selectively enabling a single lightfuzz module. | 5 | httpx, lightfuzz, paramminer_getparams, portfilter, reflected_parameters | -| nuclei | nuclei | Run nuclei scans against all discovered targets | 3 | httpx, nuclei, portfilter | -| nuclei-budget | nuclei | Run nuclei scans against all discovered targets, using budget mode to look for low hanging fruit with greatly reduced number of requests | 3 | httpx, nuclei, portfilter | -| nuclei-intense | nuclei | Run nuclei scans against all discovered targets, allowing for spidering, against ALL URLs, and with additional discovery modules. | 6 | httpx, nuclei, portfilter, robots, urlscan, wayback | -| nuclei-technology | nuclei | Run nuclei scans against all discovered targets, running templates which match discovered technologies | 3 | httpx, nuclei, portfilter | -| paramminer | web | Discover new web parameters via brute-force, and analyze them with additional modules | 6 | httpx, hunt, paramminer_cookies, paramminer_getparams, paramminer_headers, reflected_parameters | -| spider | | Recursive web spider | 1 | httpx | -| spider-intense | | Recursive web spider with more aggressive settings | 1 | httpx | -| subdomain-enum | | Enumerate subdomains via APIs, brute-force | 51 | anubisdb, asn, azure_realm, azure_tenant, baddns_direct, baddns_zone, bevigil, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, crt, crt_db, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, shodan_idb, sitedossier, social, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, virustotal, wayback | -| tech-detect | | Detect technologies via Nuclei, and FingerprintX | 3 | fingerprintx, httpx, nuclei | -| web-basic | | Quick web scan | 18 | azure_realm, baddns, badsecrets, bucket_amazon, bucket_firebase, bucket_google, bucket_microsoft, ffuf_shortnames, filedownload, git, graphql_introspection, httpx, iis_shortnames, ntlm, oauth, robots, securitytxt, sslcert | -| web-screenshots | | Take screenshots of webpages | 3 | gowitness, httpx, social | -| web-thorough | | Aggressive web scan | 32 | ajaxpro, aspnet_bin_exposure, azure_realm, baddns, badsecrets, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_microsoft, bypass403, dotnetnuke, ffuf_shortnames, filedownload, generic_ssrf, git, graphql_introspection, host_header, httpx, hunt, iis_shortnames, lightfuzz, ntlm, oauth, reflected_parameters, retirejs, robots, securitytxt, smuggler, sslcert, telerik, url_manipulation | +| Preset | Category | Description | # Modules | Modules | +|-------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|---------------------------------------------------------------------------------------------| +| baddns | | Check for subdomain takeovers and other DNS issues. | 1 | baddns | +| baddns-heavy | | Run all baddns modules and submodules. | 3 | baddns, baddns_direct, baddns_zone | +| cloud-enum | | Enumerate cloud resources such as storage buckets, etc. | 0 | | +| code-enum | | Enumerate Git repositories, Docker images, etc. | 0 | | +| dirbust-heavy | web | Recursive web directory brute-force (aggressive) | 3 | ffuf, httpx, wayback | +| dirbust-light | web | Basic web directory brute-force (surface-level directories only) | 1 | ffuf | +| dotnet-audit | web | Comprehensive scan for all IIS/.NET specific modules and module settings | 8 | ajaxpro, aspnet_bin_exposure, badsecrets, dotnetnuke, ffuf, ffuf_shortnames, httpx, telerik | +| email-enum | | Enumerate email addresses from APIs, web crawling, etc. | 0 | | +| fast | | Scan only the provided targets as fast as possible - no extra discovery | 0 | | +| iis-shortnames | web | Recursively enumerate IIS shortnames | 0 | | +| kitchen-sink | | Everything everywhere all at once | 7 | baddns, baddns_direct, baddns_zone, ffuf, httpx, hunt, reflected_parameters | +| lightfuzz | web | Default fuzzing: all 9 submodules (cmdi, crypto, path, serial, sqli, ssti, xss, esi, ssrf) plus companion modules (badsecrets, hunt, reflected_parameters). POST fuzzing disabled but try_post_as_get enabled, so POST params are retested as GET. Skips confirmed WAFs. | 6 | badsecrets, httpx, hunt, lightfuzz, portfilter, reflected_parameters | +| lightfuzz-heavy | web | Aggressive fuzzing: everything in lightfuzz, plus paramminer brute-force parameter discovery (headers, GET params, cookies), POST request fuzzing enabled, try_get_as_post enabled (GET params retested as POST), and robots.txt parsing. Still skips confirmed WAFs. | 7 | badsecrets, httpx, hunt, lightfuzz, portfilter, reflected_parameters, robots | +| lightfuzz-light | web | Minimal fuzzing: only path traversal, SQLi, and XSS submodules. No POST requests. No companion modules. Safest option for running alongside larger scans with minimal overhead. | 3 | httpx, lightfuzz, portfilter | +| lightfuzz-max | web | Maximum fuzzing: everything in lightfuzz-heavy, plus WAF targets are no longer skipped, each unique parameter-value pair is fuzzed individually (no collapsing), common headers like X-Forwarded-For are fuzzed even if not observed, and potential parameters are speculated from JSON/XML response bodies. Significantly increases scan time. | 7 | badsecrets, httpx, hunt, lightfuzz, portfilter, reflected_parameters, robots | +| lightfuzz-xss | web | XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST disabled, no query string collapsing. Example of a focused single-submodule preset. | 5 | httpx, lightfuzz, paramminer_getparams, portfilter, reflected_parameters | +| nuclei | nuclei | Run nuclei scans against all discovered targets | 3 | httpx, nuclei, portfilter | +| nuclei-budget | nuclei | Run nuclei scans against all discovered targets, using budget mode to look for low hanging fruit with greatly reduced number of requests | 3 | httpx, nuclei, portfilter | +| nuclei-heavy | nuclei | Run nuclei scans against all discovered targets, allowing for spidering, against ALL URLs, and with additional discovery modules. | 6 | httpx, nuclei, portfilter, robots, urlscan, wayback | +| nuclei-technology | nuclei | Run nuclei scans against all discovered targets, running templates which match discovered technologies | 3 | httpx, nuclei, portfilter | +| paramminer | web | Discover new web parameters via brute-force, and analyze them with additional modules | 3 | httpx, hunt, reflected_parameters | +| spider | | Recursive web spider | 1 | httpx | +| spider-heavy | | Recursive web spider with more aggressive settings | 1 | httpx | +| subdomain-enum | | Enumerate subdomains via APIs, brute-force | 0 | | +| tech-detect | | Detect technologies via Nuclei, and FingerprintX | 2 | fingerprintx, nuclei | +| web | | Quick web scan | 0 | | +| web-heavy | | Aggressive web scan | 0 | | +| web-screenshots | | Take screenshots of webpages | 0 | | <!-- END BBOT PRESETS --> diff --git a/docs/scanning/tips_and_tricks.md b/docs/scanning/tips_and_tricks.md index 52589c4aa7..eb7ae16ed3 100644 --- a/docs/scanning/tips_and_tricks.md +++ b/docs/scanning/tips_and_tricks.md @@ -108,24 +108,6 @@ config: bbot -t evilcorp.com -p skip_cdns.yml ``` -### Ingest BBOT Data Into SIEM (Elastic, Splunk) - -If your goal is to run a BBOT scan and later feed its data into a SIEM such as Elastic, be sure to enable this option when scanning: - -```bash -bbot -t evilcorp.com -c modules.json.siem_friendly=true -``` - -This ensures the `.data` event attribute is always the same type (a dictionary), by nesting it like so: -```json -{ - "type": "DNS_NAME", - "data": { - "DNS_NAME": "blacklanternsecurity.com" - } -} -``` - ### Custom HTTP Proxy Web pentesters may appreciate BBOT's ability to quickly populate Burp Suite site maps for all subdomains in a target. If your scan includes gowitness, this will capture the traffic as if you manually visited each website in your browser -- including auxiliary web resources and javascript API calls. To accomplish this, set the `web.http_proxy` config option like so: @@ -152,20 +134,28 @@ By default, BBOT only shows in-scope events (with a few exceptions for things li bbot -f subdomain-enum -t evilcorp.com -c scope.report_distance=2 ~~~ -### Speed Up Scans By Disabling DNS Resolution +### Speed Up Scans with `--fast-mode` + +If you have a ready list of hosts/urls and just want to scan them as fast as possible without any extra discovery, use `--fast-mode`. It's a CLI alias for `--preset fast`, which disables non-essential speculation and DNS resolution: + +```yaml +--8<-- "bbot/presets/fast.yml" +``` If you already have a list of discovered targets (e.g. URLs), you can speed up the scan by skipping BBOT's DNS resolution. You can do this by setting `dns.disable` to `true`: +If you don't care about DNS-based scope checks, you can go even further by completely disabling DNS resolution: + ~~~bash # completely disable DNS resolution -bbot -m httpx gowitness wappalyzer -t urls.txt -c dns.disable=true +bbot -m httpx gowitness -t urls.txt -c dns.disable=true ~~~ -Note that the above setting _completely_ disables DNS resolution, meaning even `A` and `AAAA` records are not resolved. This can cause problems if you're using an IP whitelist or blacklist. In this case, you'll want to use `dns.minimal` instead: +Note that the above setting _completely_ disables DNS, meaning even `A` and `AAAA` records are not resolved. This can cause problems if you're using an IP whitelist or blacklist. In this case, you'll want to use `dns.minimal` instead: ~~~bash # only resolve A and AAAA records -bbot -m httpx gowitness wappalyzer -t urls.txt -c dns.minimal=true +bbot -m httpx gowitness -t urls.txt -c dns.minimal=true ~~~ ## FAQ diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 4a1c42e951..0000000000 --- a/poetry.lock +++ /dev/null @@ -1,4303 +0,0 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. - -[[package]] -name = "annotated-doc" -version = "0.0.4" -description = "Document parameters, class attributes, return types, and variables inline, with Annotated." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, - {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "ansible-core" -version = "2.15.13" -description = "Radically simple IT automation" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "ansible_core-2.15.13-py3-none-any.whl", hash = "sha256:e7f50bbb61beae792f5ecb86eff82149d3948d078361d70aedb01d76bc483c30"}, - {file = "ansible_core-2.15.13.tar.gz", hash = "sha256:f542e702ee31fb049732143aeee6b36311ca48b7d13960a0685afffa0d742d7f"}, -] - -[package.dependencies] -cryptography = "*" -importlib-resources = {version = ">=5.0,<5.1", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0.0" -packaging = "*" -PyYAML = ">=5.1" -resolvelib = ">=0.5.3,<1.1.0" - -[[package]] -name = "ansible-runner" -version = "2.4.2" -description = "\"Consistent Ansible Python API and CLI with container and process isolation runtime capabilities\"" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "ansible_runner-2.4.2-py3-none-any.whl", hash = "sha256:0bde6cb39224770ff49ccdc6027288f6a98f4ed2ea0c64688b31217033221893"}, - {file = "ansible_runner-2.4.2.tar.gz", hash = "sha256:331d4da8d784e5a76aa9356981c0255f4bb1ba640736efe84b0bd7c73a4ca420"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.6,<6.3", markers = "python_version < \"3.10\""} -packaging = "*" -pexpect = ">=4.5" -python-daemon = "*" -pyyaml = "*" - -[[package]] -name = "antlr4-python3-runtime" -version = "4.9.3" -description = "ANTLR 4.9.3 runtime for Python 3.7" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b"}, -] - -[[package]] -name = "anyio" -version = "4.12.1" -description = "High-level concurrency and networking framework on top of asyncio or Trio" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, - {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] - -[[package]] -name = "babel" -version = "2.18.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, - {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, -] - -[package.extras] -dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] - -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] - -[[package]] -name = "backrefs" -version = "6.1" -description = "A wrapper around re and regex that adds additional back references." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1"}, - {file = "backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7"}, - {file = "backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a"}, - {file = "backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05"}, - {file = "backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853"}, - {file = "backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0"}, - {file = "backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231"}, -] - -[package.extras] -extras = ["regex"] - -[[package]] -name = "beautifulsoup4" -version = "4.14.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.7.0" -groups = ["main", "docs"] -files = [ - {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}, - {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}, -] - -[package.dependencies] -soupsieve = ">=1.6.1" -typing-extensions = ">=4.0.0" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "cachetools" -version = "6.2.6" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda"}, - {file = "cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6"}, -] - -[[package]] -name = "certifi" -version = "2026.1.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev", "docs"] -files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, -] - -[[package]] -name = "cffi" -version = "2.0.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\" or implementation_name == \"pypy\"" -files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, -] - -[package.dependencies] -pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "cfgv" -version = "3.5.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, - {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main", "docs"] -files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, -] - -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -groups = ["dev", "docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "click" -version = "8.3.1" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -groups = ["dev", "docs"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, - {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "cloudcheck" -version = "9.3.0" -description = "Detailed database of cloud providers. Instantly look up a domain or IP address" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59199ed17b14ca87220ad4b13ca38999a36826a63fc3a86f6274289c3247bddb"}, - {file = "cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e9f3b13eafafde34be9f1ca2aca897f6bbaf955c04144e42c3877228b3569f3"}, - {file = "cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e6aeea4742501dde2b7815877a925da0b1463e51ebae819b5868f46ceb68024"}, - {file = "cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7bd368a8417e67a7313f276429d1fcf3f4fb2ee6604e4e708ac65112f22aac5"}, - {file = "cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9722d5dafcbb56152c0fd32d19573e5dd91d6f6d07981d0ef0fca9ae47900eb"}, - {file = "cloudcheck-9.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b83396008638a6efd631b25b435f31b758732fae97beb5fef5fa1997619ede0d"}, - {file = "cloudcheck-9.3.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:43d38b7195929e19287bf7e9c0155b8dd3cafaebddc642d31b96629c05d775c0"}, - {file = "cloudcheck-9.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ee2c52294285087b5f65715cdd8fc97358cce25af88ed265c1a39c9ac407cb2c"}, - {file = "cloudcheck-9.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:07e8dba045fc365f316849d4caac8c06886c5eb602fc9239067822c0ef6a8737"}, - {file = "cloudcheck-9.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:3b88fb61d8242ef1801d61177849a168a6427b4b113e5d2f4787c428a862a113"}, - {file = "cloudcheck-9.3.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e765635136318808997deb3e633a35cde914479003321de21654a0f1b03b8820"}, - {file = "cloudcheck-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e275ee18f4991e50476971de5986fe42dc9180e66fd04f853d1c1adf4457379b"}, - {file = "cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e8b26d6f57c8c4a95491235ebe31ece0d24c33c18e1226293cc47437b6b4d3"}, - {file = "cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1f61946050445be504dd9a2875fc15109d24d99f79b8925b2a8babaa62079ca2"}, - {file = "cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2f08ad1719d485d4049c6ad4c2867b979f9f2d8002459baf7b6f8e364ec6b78"}, - {file = "cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bc6c167bb0be90933f0c5907a4d3a82d23a02bb71aaab378fd8d6b76eac585"}, - {file = "cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5322e9aaf54e9d664436a305067976b7c1cff50a7dd2648f593bb4f02bfea9a"}, - {file = "cloudcheck-9.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9be898d7a98105f25e292c6f958ad862c5915c95c1628dc6dcdf7c9f9db404fd"}, - {file = "cloudcheck-9.3.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d3ee8c28efc9fc69122cfbec0b1dfc72469d905227f4cccaee490b8c725b88"}, - {file = "cloudcheck-9.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:becc2a61d07b8364280f33fc5540ddaf6c9d96f50ac5b1de0922036a76c685af"}, - {file = "cloudcheck-9.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:158e34819223485ed365a2f4f98b17029591a895869739afd9c5d64bfea68a09"}, - {file = "cloudcheck-9.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:b33bf641c96b03513c508dac40e0042dd260ae9c4ae4bcdfcbef92a91d5e4dc3"}, - {file = "cloudcheck-9.3.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4ce814065be3f6341b63d0a34e1a8fbfcd294f911d2eef87c421f0ddb21f7c93"}, - {file = "cloudcheck-9.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60be463311be5d4525acce03aff8795c8eebb30bea4da1a5451a468812a134c7"}, - {file = "cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cb382f9da19fe24b300cdbb10aa44d14577d7cd5b20ff6ebc0fe0bad3b8e29"}, - {file = "cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da69db51d1c3b4a87a519d301f742ac52f355071a2f1acbbc65e4fc3ac7f314d"}, - {file = "cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68ae114b27342d0fe265aee543c154a1458be6dfea4aa9f49038870c6ede45ad"}, - {file = "cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5c71932bb407e1c39f275ef0d9cc0cf20f88fd1fac259b35641db91e9618b36"}, - {file = "cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c90d96a4d414e2f418ed6fbd39a93550de8e51c55788673a46410f020916616e"}, - {file = "cloudcheck-9.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9aedfac28ff9994c1dde5d89bba7d9a263c7d1e3a934ed62a8ae3ed48e851fb6"}, - {file = "cloudcheck-9.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:36d9afdd811998cbaebd3638e142453b2d82d5b6aeb0bfb6a41582cb9962ea4a"}, - {file = "cloudcheck-9.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ac1ff7eefaf892a67f8fad7a651a07ad530faddd9c5848261dc53a3a331045c6"}, - {file = "cloudcheck-9.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee329c0996ebf0e245581a0707e5ee828fed5b761bdcd69577bc4ab4808a29d7"}, - {file = "cloudcheck-9.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cfc70425ba37fae7a44a66a3833ef994b99f039c5a621f523852f61b6eb320c7"}, - {file = "cloudcheck-9.3.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ed2e9171a41786f2454902b209fe999146dc2991c1d7d0ed68fe86bbb177552a"}, - {file = "cloudcheck-9.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1651903604090d5f4dc671c243383e87cd0ab53d7a34d4e7887d82e9f2077a28"}, - {file = "cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05ec385d95adef0a420a51a1df97d17b6c29d3030b2f2b1ffca5de1ea85ee7a5"}, - {file = "cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c477506721b87d7e0a6a13386bd57feb5ab1615cbcdd9d62971640df24ba70cc"}, - {file = "cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a996011efef6af71f2f712fbe9bc9fefd49216c0dffc648528abd329f6003a0"}, - {file = "cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af152cf8e1b2a845db3412b131d6c8c6964cff161aad9500b56bd383ec028936"}, - {file = "cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:359e7c66015d3245d636ce03aa527bf6d69c2e0f72278857a2b51e9673df9904"}, - {file = "cloudcheck-9.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a8138b78e7a49814ef6bf56f0de9b35c1e53473fd83cabb451db3e740ab5e83"}, - {file = "cloudcheck-9.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:22f3645c1eb67a3489c7ebbfef4eb3c1f39187ab54a5d61703cb26df8b477d38"}, - {file = "cloudcheck-9.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:78b2f7d8235f9d5fe2d3670c125769c65b94cca1e0170d682069bb478b20ffc8"}, - {file = "cloudcheck-9.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:360b80aad144c2fbf8cf251587af714f51d58b02e76593d60da40b20a6ba6140"}, - {file = "cloudcheck-9.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:d623b523de9d24297fc6d337302e12faf8ead6c5ab17bcbf39cbed1ec7f7abe1"}, - {file = "cloudcheck-9.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2033d75451653babb908394f00a78ead9cb66481f7ca88f957b74fdff050a0b9"}, - {file = "cloudcheck-9.3.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b504627920b80cc4695b881a1e58be109abdc482be8202865d11c028865ff7e3"}, - {file = "cloudcheck-9.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0edb7e05e289852ca026cfa97fea8c86d369a3a6a061edeaf47939a31c745cc2"}, - {file = "cloudcheck-9.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:99509b9cefc95cff71bb0cda4651ec3b931202512c41583940e471038cb0f288"}, - {file = "cloudcheck-9.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:138e6578db91123a2aafc21a7ee89d302ceec49891b1257364832cd9a4f5ad62"}, - {file = "cloudcheck-9.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4058bbda0b578853288af0bb58de52257cfcafd40b8609a199d5d2b71ec773d9"}, - {file = "cloudcheck-9.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8eb3e1af683f327f0eb7dbe1fc93fb07d271b69e0045540d566830fae7855dab"}, - {file = "cloudcheck-9.3.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b4415fd866000257dae58d9b5ab58fb2c95225b65e770f3badee33d3ae4c2989"}, - {file = "cloudcheck-9.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:530874ef87665d6e14b4756b85b95a4c27916804a6778125851b49203ae037c4"}, - {file = "cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d37ed257e26a21389b99b1c7ad414c3d24b56eab21686a549f8ebf2bdc1dd48"}, - {file = "cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3fcb7b0332656c9166bc09977559bad260df9dcb6bcac3baa980842c2017a4"}, - {file = "cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89347b3e458262119a7f94d5ff2e0d535996a6dd7b501a222a28b8b235379e40"}, - {file = "cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:252fd606307b4a039b34ff928d482302b323217d92b94eadc9019c81f1231e61"}, - {file = "cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86a9b96fcd645e028980db0e25482b1af13300c5e4e76fcd6707adffd9552220"}, - {file = "cloudcheck-9.3.0-cp314-cp314-manylinux_2_38_x86_64.whl", hash = "sha256:c055966a04d21b4728e525633d7f0ff5713b76bac9024679ab20ff2e8050e5ba"}, - {file = "cloudcheck-9.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3171964cb5e204d17192cf12b79210b0f36457786af187c89407eae297f015fe"}, - {file = "cloudcheck-9.3.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4fdb2cb2727f40e5e4d66a3c43895f0892c72f9615142a190271d9b91dc634c5"}, - {file = "cloudcheck-9.3.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fa2352c765342aefa2c0e6a75093efb75fafaab51f86e36c4b32849e0ef18ee8"}, - {file = "cloudcheck-9.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:146c545702267071e1a375d8ca8bbd7a4fa5e0f87ac6adfd13fc8835bb3d2bc7"}, - {file = "cloudcheck-9.3.0-cp314-cp314-win32.whl", hash = "sha256:a95b840efe2616231c99a9ef5be4e8484b880af5e3e9eeab81bf473cbee70088"}, - {file = "cloudcheck-9.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:9d631e21b3945615739f7862e1e378b2f3f43d4409a62bc657e858762f83ac67"}, - {file = "cloudcheck-9.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:681ef11beeebfbf6205b0e05a6d151943a533e6e6124f0399b9128692b350c63"}, - {file = "cloudcheck-9.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f96f1ebc2b30c7c790b62f1a7c13909230502c457b445cd96d1803c4434da6bb"}, - {file = "cloudcheck-9.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5895e5dd24a3e9f1d3412e15ff96fdd0a6f58d0a1ea192deba6f02926331054"}, - {file = "cloudcheck-9.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8407b08b382a6bcb23ab77ce3a12dfdf075feff418c911f7a385701a20b8df34"}, - {file = "cloudcheck-9.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4dab2a77055204e014c31cf7466f23687612de66579b81e3c18c00d3eeaa526b"}, - {file = "cloudcheck-9.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:68088d71a16ac55390ec53240642b3cf26f98692035e1ed425a3c35144ca1f26"}, - {file = "cloudcheck-9.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5615d361881b15f36afd88136bc5af221ff1794b18c49616411c32578a69de28"}, - {file = "cloudcheck-9.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d93616c9d1651c5fc76aeafeb5fe854ea99a2a3c72b5cfc658c852f73e0adef7"}, - {file = "cloudcheck-9.3.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d2fcdce20139ade4aedfce08d8bbab036178ce0bd3e3eb7402e01d98662d84e"}, - {file = "cloudcheck-9.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac36685a49614deec545d2048015c3f0998777df3678a09e865dade3f0157fc4"}, - {file = "cloudcheck-9.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8820f6ba9fe3ecd13b52600b4784e09a9f8c39e0a625d5c1365d5a362996bd13"}, - {file = "cloudcheck-9.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497131e592ab84f817ebe47cce604653f32d764bb28bf44cd69f7b4d8a9e004"}, - {file = "cloudcheck-9.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3d77f037d0149d839e5d642f7ecdc83db686031081a006695eed74bb958baf09"}, - {file = "cloudcheck-9.3.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:47b591ef041ed9e333af98f186e3ce56f8c486e1fc91afb1a3633d43f28e34b8"}, - {file = "cloudcheck-9.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3e48a176278b6e75638502a64b90b3ee9bba6a198c229ba8f1485e9eed527d20"}, - {file = "cloudcheck-9.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1d52f55834bf34bb75d0934ef60046e7ee584db09b2e269ef9170e73f8ddd45"}, - {file = "cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee5e5b240d39c14829576b841411d6a4dd867c1c1d4f23e5aadf910151b25ed1"}, - {file = "cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d5fe3f4e1fef0a83ffd2bfa2d4aa8b4c83aea2c7116fb83e75dcf82780aeb5dd"}, - {file = "cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39ec7ebae9a8a1ec42d5ec047d0506584576aa1cb32d7d4c3fff5db241844ebe"}, - {file = "cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92eb00480d651e8ee7d922005804dcc10a30d05e8a1feb7d49d93e11dd7b7b82"}, - {file = "cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0befb18f1b563985c68ce3aae0d58363939af66e13a15f0defbab1a2bd512459"}, - {file = "cloudcheck-9.3.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:66940758bf97c40db14c1f7457ece633503a91803191c84742f3a938b5fbc9d8"}, - {file = "cloudcheck-9.3.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:5d97d3ecd2917b75b518766281be408253f6f59a2d09db54f7ecf9d847c6de3a"}, - {file = "cloudcheck-9.3.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:f593b1a400f8b0ec3995b56126efb001729b46bac4c76d6d2399e8ab62e49515"}, - {file = "cloudcheck-9.3.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4854fc0aa88ec38275f0c2f8057803c1c37eec93d9f4c5e9f0e0a5b38fd6604f"}, - {file = "cloudcheck-9.3.0.tar.gz", hash = "sha256:e4f92690f84b176395d01a0694263d8edb0f8fd3a63100757376b7810879e6f5"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev", "docs"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.10.7" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, - {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, - {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, - {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, - {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, - {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, - {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, - {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, - {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, - {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, - {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, - {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, - {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, - {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, - {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, - {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, - {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, - {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, - {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, - {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, - {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, - {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, - {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, - {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, - {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, - {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - -[[package]] -name = "coverage" -version = "7.13.4" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, - {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, - {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, - {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, - {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, - {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, - {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, - {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, - {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, - {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, - {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, - {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, - {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, - {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, - {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, - {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, - {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, - {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, - {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, - {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, - {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, - {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, - {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, - {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, - {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, - {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, - {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, - {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, - {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, - {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, - {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, - {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, - {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, - {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, - {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, - {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - -[[package]] -name = "cryptography" -version = "43.0.3" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = ">=3.7" -groups = ["main"] -markers = "python_version == \"3.9\"" -files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, -] - -[package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "cryptography" -version = "46.0.4" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" -groups = ["main"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616"}, - {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0"}, - {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0"}, - {file = "cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5"}, - {file = "cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b"}, - {file = "cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f"}, - {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82"}, - {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c"}, - {file = "cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061"}, - {file = "cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7"}, - {file = "cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019"}, - {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4"}, - {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b"}, - {file = "cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc"}, - {file = "cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3"}, - {file = "cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} -typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.4)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "deepdiff" -version = "8.6.1" -description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b"}, - {file = "deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a"}, -] - -[package.dependencies] -orderly-set = ">=5.4.1,<6" - -[package.extras] -cli = ["click (>=8.1.0,<8.2.0)", "pyyaml (>=6.0.0,<6.1.0)"] -coverage = ["coverage (>=7.6.0,<7.7.0)"] -dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)", "jsonpickle (>=4.0.0,<4.1.0)", "nox (==2025.5.1)", "numpy (>=2.0,<3.0) ; python_version < \"3.10\"", "numpy (>=2.2.0,<2.3.0) ; python_version >= \"3.10\"", "orjson (>=3.10.0,<3.11.0)", "pandas (>=2.2.0,<2.3.0)", "polars (>=1.21.0,<1.22.0)", "python-dateutil (>=2.9.0,<2.10.0)", "tomli (>=2.2.0,<2.3.0)", "tomli-w (>=1.2.0,<1.3.0)", "uuid6 (==2025.0.1)"] -docs = ["Sphinx (>=6.2.0,<6.3.0)", "sphinx-sitemap (>=2.6.0,<2.7.0)", "sphinxemoji (>=0.3.0,<0.4.0)"] -optimize = ["orjson"] -static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)", "pydantic (>=2.10.0,<2.11.0)"] -test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] - -[[package]] -name = "distlib" -version = "0.4.0" -description = "Distribution utilities" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, - {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, -] - -[[package]] -name = "dnspython" -version = "2.7.0" -description = "DNS toolkit" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, - {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, -] - -[package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=43)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=1.0.0)"] -idna = ["idna (>=3.7)"] -trio = ["trio (>=0.23)"] -wmi = ["wmi (>=1.5.1)"] - -[[package]] -name = "dunamai" -version = "1.26.0" -description = "Dynamic version generation" -optional = false -python-versions = ">=3.5" -groups = ["dev"] -files = [ - {file = "dunamai-1.26.0-py3-none-any.whl", hash = "sha256:f584edf0fda0d308cce0961f807bc90a8fe3d9ff4d62f94e72eca7b43f0ed5f6"}, - {file = "dunamai-1.26.0.tar.gz", hash = "sha256:5396ac43aa20ed059040034e9f9798c7464cf4334c6fc3da3732e29273a2f97d"}, -] - -[package.dependencies] -packaging = ">=20.9" - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, - {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "fastapi" -version = "0.128.8" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1"}, - {file = "fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007"}, -] - -[package.dependencies] -annotated-doc = ">=0.0.2" -pydantic = ">=2.7.0" -starlette = ">=0.40.0,<1.0.0" -typing-extensions = ">=4.8.0" -typing-inspection = ">=0.4.2" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.9.3)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=5.8.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] -standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "filelock" -version = "3.19.1" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, - {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, -] - -[[package]] -name = "filelock" -version = "3.20.3" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, - {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -optional = false -python-versions = "*" -groups = ["docs"] -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "griffe" -version = "1.14.0" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -optional = false -python-versions = ">=3.9" -groups = ["dev", "docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0"}, - {file = "griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13"}, -] - -[package.dependencies] -colorama = ">=0.4" - -[[package]] -name = "griffe" -version = "1.15.0" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -optional = false -python-versions = ">=3.10" -groups = ["dev", "docs"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3"}, - {file = "griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea"}, -] - -[package.dependencies] -colorama = ">=0.4" - -[package.extras] -pypi = ["pip (>=24.0)", "platformdirs (>=4.2)", "wheel (>=0.42)"] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.16" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.28.1" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" - -[package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "identify" -version = "2.6.15" -description = "File identification library for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"}, - {file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "identify" -version = "2.6.16" -description = "File identification library for Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0"}, - {file = "identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.11" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev", "docs"] -files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "6.2.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev", "docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-6.2.1-py3-none-any.whl", hash = "sha256:f65e478a7c2177bd19517a3a15dac094d253446d8690c5f3e71e735a04312374"}, - {file = "importlib_metadata-6.2.1.tar.gz", hash = "sha256:5a66966b39ff1c14ef5b2d60c1d842b0141fefff0f4cc6365b4bc9446c652807"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8 ; python_version < \"3.12\"", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["docs"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] - -[[package]] -name = "importlib-resources" -version = "5.0.7" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.6" -groups = ["main", "docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_resources-5.0.7-py3-none-any.whl", hash = "sha256:2238159eb743bd85304a16e0536048b3e991c531d1cd51c4a834d1ccf2829057"}, - {file = "importlib_resources-5.0.7.tar.gz", hash = "sha256:4df460394562b4581bb4e4087ad9447bd433148fba44241754ec3152499f1d1b"}, -] - -[package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler", "pytest-flake8", "pytest-mypy ; platform_python_implementation != \"PyPy\""] - -[[package]] -name = "importlib-resources" -version = "6.5.2" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["docs"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, - {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev", "docs"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "libsass" -version = "0.23.0" -description = "Sass for Python: A straightforward binding of libsass for Python." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "libsass-0.23.0-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:34cae047cbbfc4ffa832a61cbb110f3c95f5471c6170c842d3fed161e40814dc"}, - {file = "libsass-0.23.0-cp38-abi3-macosx_14_0_arm64.whl", hash = "sha256:ea97d1b45cdc2fc3590cb9d7b60f1d8915d3ce17a98c1f2d4dd47ee0d9c68ce6"}, - {file = "libsass-0.23.0-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306"}, - {file = "libsass-0.23.0-cp38-abi3-win32.whl", hash = "sha256:31e86d92a5c7a551df844b72d83fc2b5e50abc6fbbb31e296f7bebd6489ed1b4"}, - {file = "libsass-0.23.0-cp38-abi3-win_amd64.whl", hash = "sha256:a2ec85d819f353cbe807432d7275d653710d12b08ec7ef61c124a580a8352f3c"}, - {file = "libsass-0.23.0.tar.gz", hash = "sha256:6f209955ede26684e76912caf329f4ccb57e4a043fd77fe0e7348dd9574f1880"}, -] - -[[package]] -name = "livereload" -version = "2.7.1" -description = "Python LiveReload is an awesome tool for web developers" -optional = false -python-versions = ">=3.7" -groups = ["docs"] -files = [ - {file = "livereload-2.7.1-py3-none-any.whl", hash = "sha256:5201740078c1b9433f4b2ba22cd2729a39b9d0ec0a2cc6b4d3df257df5ad0564"}, - {file = "livereload-2.7.1.tar.gz", hash = "sha256:3d9bf7c05673df06e32bea23b494b8d36ca6d10f7d5c3c8a6989608c09c986a9"}, -] - -[package.dependencies] -tornado = "*" - -[[package]] -name = "lockfile" -version = "0.12.2" -description = "Platform-independent file locking module" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, - {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, -] - -[[package]] -name = "lxml" -version = "6.0.2" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"}, - {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c"}, - {file = "lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b"}, - {file = "lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0"}, - {file = "lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5"}, - {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607"}, - {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7"}, - {file = "lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46"}, - {file = "lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078"}, - {file = "lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285"}, - {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456"}, - {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322"}, - {file = "lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849"}, - {file = "lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f"}, - {file = "lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6"}, - {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77"}, - {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314"}, - {file = "lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2"}, - {file = "lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7"}, - {file = "lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf"}, - {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe"}, - {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c"}, - {file = "lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b"}, - {file = "lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed"}, - {file = "lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8"}, - {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d"}, - {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f"}, - {file = "lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312"}, - {file = "lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca"}, - {file = "lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c"}, - {file = "lxml-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a656ca105115f6b766bba324f23a67914d9c728dafec57638e2b92a9dcd76c62"}, - {file = "lxml-6.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c54d83a2188a10ebdba573f16bd97135d06c9ef60c3dc495315c7a28c80a263f"}, - {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:1ea99340b3c729beea786f78c38f60f4795622f36e305d9c9be402201efdc3b7"}, - {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af85529ae8d2a453feee4c780d9406a5e3b17cee0dd75c18bd31adcd584debc3"}, - {file = "lxml-6.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe659f6b5d10fb5a17f00a50eb903eb277a71ee35df4615db573c069bcf967ac"}, - {file = "lxml-6.0.2-cp38-cp38-win32.whl", hash = "sha256:5921d924aa5468c939d95c9814fa9f9b5935a6ff4e679e26aaf2951f74043512"}, - {file = "lxml-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:0aa7070978f893954008ab73bb9e3c24a7c56c054e00566a21b553dc18105fca"}, - {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694"}, - {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb"}, - {file = "lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f"}, - {file = "lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304"}, - {file = "lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e"}, - {file = "lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml_html_clean"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] - -[[package]] -name = "markdown" -version = "3.9" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.9" -groups = ["dev", "docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280"}, - {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown" -version = "3.10.2" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.10" -groups = ["dev", "docs"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"}, - {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"}, -] - -[package.extras] -docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markupsafe" -version = "3.0.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev", "docs"] -files = [ - {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, - {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, - {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, - {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, - {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, - {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, - {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, - {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, - {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -optional = false -python-versions = ">=3.6" -groups = ["docs"] -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mike" -version = "2.1.3" -description = "Manage multiple versions of your MkDocs-powered documentation" -optional = false -python-versions = "*" -groups = ["docs"] -files = [ - {file = "mike-2.1.3-py3-none-any.whl", hash = "sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a"}, - {file = "mike-2.1.3.tar.gz", hash = "sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810"}, -] - -[package.dependencies] -importlib-metadata = "*" -importlib-resources = "*" -jinja2 = ">=2.7" -mkdocs = ">=1.0" -pyparsing = ">=3.0" -pyyaml = ">=5.1" -pyyaml-env-tag = "*" -verspec = "*" - -[package.extras] -dev = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] -test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] - -[[package]] -name = "mkdocs" -version = "1.6.1" -description = "Project documentation with Markdown." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, - {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} -jinja2 = ">=2.11.1" -markdown = ">=3.3.6" -markupsafe = ">=2.0.1" -mergedeep = ">=1.3.4" -mkdocs-get-deps = ">=0.2.0" -packaging = ">=20.5" -pathspec = ">=0.11.1" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.3" -description = "Automatically link across pages in MkDocs." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9"}, - {file = "mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75"}, -] - -[package.dependencies] -Markdown = ">=3.3" -markupsafe = ">=2.0.1" -mkdocs = ">=1.1" - -[[package]] -name = "mkdocs-extra-sass-plugin" -version = "0.1.0" -description = "This plugin adds stylesheets to your mkdocs site from `Sass`/`SCSS`." -optional = false -python-versions = ">=3.6" -groups = ["docs"] -files = [ - {file = "mkdocs-extra-sass-plugin-0.1.0.tar.gz", hash = "sha256:cca7ae778585514371b22a63bcd69373d77e474edab4b270cf2924e05c879219"}, - {file = "mkdocs_extra_sass_plugin-0.1.0-py3-none-any.whl", hash = "sha256:10aa086fa8ef1fc4650f7bb6927deb7bf5bbf5a2dd3178f47e4ef44546b156db"}, -] - -[package.dependencies] -beautifulsoup4 = ">=4.6.3" -libsass = ">=0.15" -mkdocs = ">=1.1" - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, - {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} -mergedeep = ">=1.3.4" -platformdirs = ">=2.2.0" -pyyaml = ">=5.1" - -[[package]] -name = "mkdocs-material" -version = "9.7.3" -description = "Documentation that simply works" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocs_material-9.7.3-py3-none-any.whl", hash = "sha256:37ebf7b4788c992203faf2e71900be3c197c70a4be9b0d72aed537b08a91dd9d"}, - {file = "mkdocs_material-9.7.3.tar.gz", hash = "sha256:e5f0a18319699da7e78c35e4a8df7e93537a888660f61a86bd773a7134798f22"}, -] - -[package.dependencies] -babel = ">=2.10" -backrefs = ">=5.7.post1" -colorama = ">=0.4" -jinja2 = ">=3.1" -markdown = ">=3.2" -mkdocs = ">=1.6" -mkdocs-material-extensions = ">=1.3" -paginate = ">=0.5" -pygments = ">=2.16" -pymdown-extensions = ">=10.2" -requests = ">=2.30" - -[package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4)"] -imaging = ["cairosvg (>=2.6)", "pillow (>=10.2)"] -recommended = ["mkdocs-minify-plugin (>=0.7)", "mkdocs-redirects (>=1.2)", "mkdocs-rss-plugin (>=1.6)"] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -description = "Extension pack for Python Markdown and MkDocs Material." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, - {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, -] - -[[package]] -name = "mkdocstrings" -version = "0.30.1" -description = "Automatic documentation from sources, for MkDocs." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82"}, - {file = "mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} -Jinja2 = ">=2.11.1" -Markdown = ">=3.6" -MarkupSafe = ">=1.1" -mkdocs = ">=1.6" -mkdocs-autorefs = ">=1.4" -pymdown-extensions = ">=6.3" - -[package.extras] -crystal = ["mkdocstrings-crystal (>=0.3.4)"] -python = ["mkdocstrings-python (>=1.16.2)"] -python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] - -[[package]] -name = "mkdocstrings-python" -version = "1.18.2" -description = "A Python handler for mkdocstrings." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d"}, - {file = "mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323"}, -] - -[package.dependencies] -griffe = ">=1.13" -mkdocs-autorefs = ">=1.4" -mkdocstrings = ">=0.30" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "mkdocstrings-python" -version = "1.19.0" -description = "A Python handler for mkdocstrings." -optional = false -python-versions = ">=3.10" -groups = ["docs"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "mkdocstrings_python-1.19.0-py3-none-any.whl", hash = "sha256:395c1032af8f005234170575cc0c5d4d20980846623b623b35594281be4a3059"}, - {file = "mkdocstrings_python-1.19.0.tar.gz", hash = "sha256:917aac66cf121243c11db5b89f66b0ded6c53ec0de5318ff5e22424eb2f2e57c"}, -] - -[package.dependencies] -griffe = ">=1.13" -mkdocs-autorefs = ">=1.4" -mkdocstrings = ">=0.30" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "mmh3" -version = "5.2.0" -description = "Python extension for MurmurHash (MurmurHash3), a set of fast and robust hash functions." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "mmh3-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc"}, - {file = "mmh3-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328"}, - {file = "mmh3-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d"}, - {file = "mmh3-5.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e"}, - {file = "mmh3-5.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515"}, - {file = "mmh3-5.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3"}, - {file = "mmh3-5.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e"}, - {file = "mmh3-5.2.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d"}, - {file = "mmh3-5.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772"}, - {file = "mmh3-5.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89"}, - {file = "mmh3-5.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2"}, - {file = "mmh3-5.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc"}, - {file = "mmh3-5.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106"}, - {file = "mmh3-5.2.0-cp310-cp310-win32.whl", hash = "sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d"}, - {file = "mmh3-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb"}, - {file = "mmh3-5.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8"}, - {file = "mmh3-5.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1"}, - {file = "mmh3-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051"}, - {file = "mmh3-5.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10"}, - {file = "mmh3-5.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c"}, - {file = "mmh3-5.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762"}, - {file = "mmh3-5.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4"}, - {file = "mmh3-5.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363"}, - {file = "mmh3-5.2.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8"}, - {file = "mmh3-5.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed"}, - {file = "mmh3-5.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646"}, - {file = "mmh3-5.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b"}, - {file = "mmh3-5.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779"}, - {file = "mmh3-5.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2"}, - {file = "mmh3-5.2.0-cp311-cp311-win32.whl", hash = "sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28"}, - {file = "mmh3-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee"}, - {file = "mmh3-5.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9"}, - {file = "mmh3-5.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be"}, - {file = "mmh3-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd"}, - {file = "mmh3-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96"}, - {file = "mmh3-5.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094"}, - {file = "mmh3-5.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037"}, - {file = "mmh3-5.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773"}, - {file = "mmh3-5.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5"}, - {file = "mmh3-5.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50"}, - {file = "mmh3-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765"}, - {file = "mmh3-5.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43"}, - {file = "mmh3-5.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4"}, - {file = "mmh3-5.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3"}, - {file = "mmh3-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c"}, - {file = "mmh3-5.2.0-cp312-cp312-win32.whl", hash = "sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49"}, - {file = "mmh3-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3"}, - {file = "mmh3-5.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0"}, - {file = "mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065"}, - {file = "mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de"}, - {file = "mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044"}, - {file = "mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73"}, - {file = "mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504"}, - {file = "mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b"}, - {file = "mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05"}, - {file = "mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814"}, - {file = "mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093"}, - {file = "mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54"}, - {file = "mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a"}, - {file = "mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908"}, - {file = "mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5"}, - {file = "mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a"}, - {file = "mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266"}, - {file = "mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5"}, - {file = "mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9"}, - {file = "mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290"}, - {file = "mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051"}, - {file = "mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081"}, - {file = "mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b"}, - {file = "mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078"}, - {file = "mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501"}, - {file = "mmh3-5.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b"}, - {file = "mmh3-5.2.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770"}, - {file = "mmh3-5.2.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110"}, - {file = "mmh3-5.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647"}, - {file = "mmh3-5.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63"}, - {file = "mmh3-5.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12"}, - {file = "mmh3-5.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22"}, - {file = "mmh3-5.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5"}, - {file = "mmh3-5.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07"}, - {file = "mmh3-5.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935"}, - {file = "mmh3-5.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7"}, - {file = "mmh3-5.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5"}, - {file = "mmh3-5.2.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384"}, - {file = "mmh3-5.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e"}, - {file = "mmh3-5.2.0-cp314-cp314-win32.whl", hash = "sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0"}, - {file = "mmh3-5.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b"}, - {file = "mmh3-5.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115"}, - {file = "mmh3-5.2.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932"}, - {file = "mmh3-5.2.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c"}, - {file = "mmh3-5.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be"}, - {file = "mmh3-5.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb"}, - {file = "mmh3-5.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65"}, - {file = "mmh3-5.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991"}, - {file = "mmh3-5.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645"}, - {file = "mmh3-5.2.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3"}, - {file = "mmh3-5.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279"}, - {file = "mmh3-5.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513"}, - {file = "mmh3-5.2.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db"}, - {file = "mmh3-5.2.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667"}, - {file = "mmh3-5.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5"}, - {file = "mmh3-5.2.0-cp314-cp314t-win32.whl", hash = "sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7"}, - {file = "mmh3-5.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d"}, - {file = "mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9"}, - {file = "mmh3-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51"}, - {file = "mmh3-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c"}, - {file = "mmh3-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65"}, - {file = "mmh3-5.2.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72"}, - {file = "mmh3-5.2.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d"}, - {file = "mmh3-5.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f"}, - {file = "mmh3-5.2.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025"}, - {file = "mmh3-5.2.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c"}, - {file = "mmh3-5.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0"}, - {file = "mmh3-5.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7"}, - {file = "mmh3-5.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a"}, - {file = "mmh3-5.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4"}, - {file = "mmh3-5.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055"}, - {file = "mmh3-5.2.0-cp39-cp39-win32.whl", hash = "sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a"}, - {file = "mmh3-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4"}, - {file = "mmh3-5.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2"}, - {file = "mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8"}, -] - -[package.extras] -benchmark = ["pymmh3 (==0.0.5)", "pyperf (==2.9.0)", "xxhash (==3.5.0)"] -docs = ["myst-parser (==4.0.1)", "shibuya (==2025.7.24)", "sphinx (==8.2.3)", "sphinx-copybutton (==0.5.2)"] -lint = ["black (==25.1.0)", "clang-format (==20.1.8)", "isort (==6.0.1)", "pylint (==3.3.7)"] -plot = ["matplotlib (==3.10.3)", "pandas (==2.3.1)"] -test = ["pytest (==8.4.1)", "pytest-sugar (==1.0.0)"] -type = ["mypy (==1.17.0)"] - -[[package]] -name = "nodeenv" -version = "1.10.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -files = [ - {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, - {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, -] - -[[package]] -name = "omegaconf" -version = "2.3.0" -description = "A flexible configuration library" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b"}, - {file = "omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7"}, -] - -[package.dependencies] -antlr4-python3-runtime = "==4.9.*" -PyYAML = ">=5.1.0" - -[[package]] -name = "orderly-set" -version = "5.5.0" -description = "Orderly set" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7"}, - {file = "orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce"}, -] - -[package.extras] -coverage = ["coverage (>=7.6.0,<7.7.0)"] -dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)"] -optimize = ["orjson"] -static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)"] -test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] - -[[package]] -name = "orjson" -version = "3.11.5" -description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f"}, - {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18"}, - {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a"}, - {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7"}, - {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401"}, - {file = "orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8"}, - {file = "orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167"}, - {file = "orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8"}, - {file = "orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9"}, - {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125"}, - {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814"}, - {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5"}, - {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880"}, - {file = "orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d"}, - {file = "orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1"}, - {file = "orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c"}, - {file = "orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d"}, - {file = "orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477"}, - {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e"}, - {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69"}, - {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3"}, - {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca"}, - {file = "orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98"}, - {file = "orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875"}, - {file = "orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe"}, - {file = "orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629"}, - {file = "orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f"}, - {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863"}, - {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228"}, - {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2"}, - {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05"}, - {file = "orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef"}, - {file = "orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583"}, - {file = "orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287"}, - {file = "orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0"}, - {file = "orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad"}, - {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829"}, - {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac"}, - {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d"}, - {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439"}, - {file = "orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499"}, - {file = "orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310"}, - {file = "orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5"}, - {file = "orjson-3.11.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1b280e2d2d284a6713b0cfec7b08918ebe57df23e3f76b27586197afca3cb1e9"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d8a112b274fae8c5f0f01954cb0480137072c271f3f4958127b010dfefaec"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0a2ae6f09ac7bd47d2d5a5305c1d9ed08ac057cda55bb0a49fa506f0d2da00"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d87bd1896faac0d10b4f849016db81a63e4ec5df38757ffae84d45ab38aa71"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:801a821e8e6099b8c459ac7540b3c32dba6013437c57fdcaec205b169754f38c"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a0f6ac618c98c74b7fbc8c0172ba86f9e01dbf9f62aa0b1776c2231a7bffe5"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea7339bdd22e6f1060c55ac31b6a755d86a5b2ad3657f2669ec243f8e3b2bdb"}, - {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4dad582bc93cef8f26513e12771e76385a7e6187fd713157e971c784112aad56"}, - {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0522003e9f7fba91982e83a97fec0708f5a714c96c4209db7104e6b9d132f111"}, - {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7403851e430a478440ecc1258bcbacbfbd8175f9ac1e39031a7121dd0de05ff8"}, - {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5f691263425d3177977c8d1dd896cde7b98d93cbf390b2544a090675e83a6a0a"}, - {file = "orjson-3.11.5-cp39-cp39-win32.whl", hash = "sha256:61026196a1c4b968e1b1e540563e277843082e9e97d78afa03eb89315af531f1"}, - {file = "orjson-3.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b94b947ac08586af635ef922d69dc9bc63321527a3a04647f4986a73f4bd30"}, - {file = "orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5"}, -] - -[[package]] -name = "packaging" -version = "26.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev", "docs"] -files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, -] - -[[package]] -name = "paginate" -version = "0.5.7" -description = "Divides large result sets into pages for easier browsing" -optional = false -python-versions = "*" -groups = ["docs"] -files = [ - {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, - {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, -] - -[package.extras] -dev = ["pytest", "tox"] -lint = ["black"] - -[[package]] -name = "pathspec" -version = "1.0.4" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, - {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, -] - -[package.extras] -hyperscan = ["hyperscan (>=0.7)"] -optional = ["typing-extensions (>=4)"] -re2 = ["google-re2 (>=1.1)"] -tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] - -[[package]] -name = "pexpect" -version = "4.9.0" -description = "Pexpect allows easy control of interactive console applications." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "platformdirs" -version = "4.4.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -groups = ["dev", "docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, - {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - -[[package]] -name = "platformdirs" -version = "4.5.1" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.10" -groups = ["dev", "docs"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, - {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, -] - -[package.extras] -docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] -type = ["mypy (>=1.18.2)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "poetry-dynamic-versioning" -version = "1.10.0" -description = "Plugin for Poetry to enable dynamic versioning based on VCS tags" -optional = false -python-versions = "<4.0,>=3.7" -groups = ["dev"] -files = [ - {file = "poetry_dynamic_versioning-1.10.0-py3-none-any.whl", hash = "sha256:a573d47c77e96661a309ee2115c9c5db4c66ce78986747479187424c8c9f5093"}, - {file = "poetry_dynamic_versioning-1.10.0.tar.gz", hash = "sha256:52bf9ed57f2d60f4250a1dfe43db7b8144541df2f3ae6e712d12b43ecda71f47"}, -] - -[package.dependencies] -dunamai = ">=1.26.0,<2.0.0" -jinja2 = ">=2.11.1,<4" -tomlkit = ">=0.4" - -[package.extras] -plugin = ["poetry (>=1.2.0)"] - -[[package]] -name = "pre-commit" -version = "4.3.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8"}, - {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pre-commit" -version = "4.5.1" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77"}, - {file = "pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "psutil" -version = "7.2.2" -description = "Cross-platform lib for process and system monitoring." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, - {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, - {file = "psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63"}, - {file = "psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312"}, - {file = "psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b"}, - {file = "psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9"}, - {file = "psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00"}, - {file = "psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9"}, - {file = "psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a"}, - {file = "psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf"}, - {file = "psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1"}, - {file = "psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841"}, - {file = "psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486"}, - {file = "psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979"}, - {file = "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9"}, - {file = "psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e"}, - {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8"}, - {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc"}, - {file = "psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988"}, - {file = "psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee"}, - {file = "psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] -test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "puremagic" -version = "1.30" -description = "Pure python implementation of magic file detection" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "puremagic-1.30-py3-none-any.whl", hash = "sha256:5eeeb2dd86f335b9cfe8e205346612197af3500c6872dffebf26929f56e9d3c1"}, - {file = "puremagic-1.30.tar.gz", hash = "sha256:f9ff7ac157d54e9cf3bff1addfd97233548e75e685282d84ae11e7ffee1614c9"}, -] - -[[package]] -name = "py-cpuinfo" -version = "9.0.0" -description = "Get CPU info with pure Python" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, - {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, -] - -[[package]] -name = "pycparser" -version = "2.23" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "(platform_python_implementation != \"PyPy\" or implementation_name == \"pypy\") and python_version == \"3.9\" and implementation_name != \"PyPy\"" -files = [ - {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, - {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, -] - -[[package]] -name = "pycparser" -version = "3.0" -description = "C parser in Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "python_version >= \"3.10\" and (platform_python_implementation != \"PyPy\" or implementation_name == \"pypy\") and implementation_name != \"PyPy\"" -files = [ - {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, - {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, -] - -[[package]] -name = "pycryptodome" -version = "3.23.0" -description = "Cryptographic library for Python" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] -files = [ - {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, - {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, - {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, - {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, - {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, - {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, - {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, - {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, - {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, - {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, - {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, - {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, - {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, - {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, - {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, - {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, - {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, - {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, - {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, - {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["dev", "docs"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyjwt" -version = "2.11.0" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"}, - {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"}, -] - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] - -[[package]] -name = "pymdown-extensions" -version = "10.21" -description = "Extension pack for Python Markdown." -optional = false -python-versions = ">=3.9" -groups = ["dev", "docs"] -files = [ - {file = "pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f"}, - {file = "pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5"}, -] - -[package.dependencies] -markdown = ">=3.6" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.19.1)"] - -[[package]] -name = "pyparsing" -version = "3.3.2" -description = "pyparsing - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, - {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "8.4.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, - {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "1.2.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99"}, - {file = "pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57"}, -] - -[package.dependencies] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} -pytest = ">=8.2,<9" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-benchmark" -version = "5.2.3" -description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803"}, - {file = "pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779"}, -] - -[package.dependencies] -py-cpuinfo = "*" -pytest = ">=8.1" - -[package.extras] -aspect = ["aspectlib"] -elasticsearch = ["elasticsearch"] -histogram = ["pygal", "pygaljs", "setuptools"] - -[[package]] -name = "pytest-cov" -version = "7.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, - {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, -] - -[package.dependencies] -coverage = {version = ">=7.10.6", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=7" - -[package.extras] -testing = ["process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-env" -version = "1.1.5" -description = "pytest plugin that allows you to add environment variables." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30"}, - {file = "pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf"}, -] - -[package.dependencies] -pytest = ">=8.3.3" -tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "pytest-mock (>=3.14)"] - -[[package]] -name = "pytest-httpserver" -version = "1.1.3" -description = "pytest-httpserver is a httpserver for pytest" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9"}, - {file = "pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec"}, -] - -[package.dependencies] -Werkzeug = ">=2.0.0" - -[[package]] -name = "pytest-httpserver" -version = "1.1.4" -description = "pytest-httpserver is a httpserver for pytest" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "pytest_httpserver-1.1.4-py3-none-any.whl", hash = "sha256:5dc73beae8cef139597cfdaab1b7f6bfe3551dd80965a6039e08498796053331"}, - {file = "pytest_httpserver-1.1.4.tar.gz", hash = "sha256:4d357402ae7e141f3914ed7cd25f3e24746ae928792dad60053daee4feae81fc"}, -] - -[package.dependencies] -Werkzeug = ">=2.0.0" - -[[package]] -name = "pytest-httpx" -version = "0.35.0" -description = "Send responses to httpx." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744"}, - {file = "pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f"}, -] - -[package.dependencies] -httpx = "==0.28.*" -pytest = "==8.*" - -[package.extras] -testing = ["pytest-asyncio (==0.24.*)", "pytest-cov (==6.*)"] - -[[package]] -name = "pytest-rerunfailures" -version = "16.0.1" -description = "pytest plugin to re-run tests to eliminate flaky failures" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "pytest_rerunfailures-16.0.1-py3-none-any.whl", hash = "sha256:0bccc0e3b0e3388275c25a100f7077081318196569a121217688ed05e58984b9"}, - {file = "pytest_rerunfailures-16.0.1.tar.gz", hash = "sha256:ed4b3a6e7badb0a720ddd93f9de1e124ba99a0cb13bc88561b3c168c16062559"}, -] - -[package.dependencies] -packaging = ">=17.1" -pytest = ">=7.4,<8.2.2 || >8.2.2" - -[[package]] -name = "pytest-rerunfailures" -version = "16.1" -description = "pytest plugin to re-run tests to eliminate flaky failures" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "pytest_rerunfailures-16.1-py3-none-any.whl", hash = "sha256:5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86"}, - {file = "pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e"}, -] - -[package.dependencies] -packaging = ">=17.1" -pytest = ">=7.4,<8.2.2 || >8.2.2" - -[[package]] -name = "pytest-timeout" -version = "2.4.0" -description = "pytest plugin to abort hanging tests" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, - {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[[package]] -name = "python-daemon" -version = "3.1.2" -description = "Library to implement a well-behaved Unix daemon process." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "python_daemon-3.1.2-py3-none-any.whl", hash = "sha256:b906833cef63502994ad48e2eab213259ed9bb18d54fa8774dcba2ff7864cec6"}, - {file = "python_daemon-3.1.2.tar.gz", hash = "sha256:f7b04335adc473de877f5117e26d5f1142f4c9f7cd765408f0877757be5afbf4"}, -] - -[package.dependencies] -lockfile = ">=0.10" - -[package.extras] -build = ["build", "changelog-chug", "docutils", "python-daemon[doc]", "wheel"] -devel = ["python-daemon[dist,test]"] -dist = ["python-daemon[build]", "twine"] -static-analysis = ["isort (>=5.13,<6.0)", "pip-check", "pycodestyle (>=2.12,<3.0)", "pydocstyle (>=6.3,<7.0)", "pyupgrade (>=3.17,<4.0)"] -test = ["coverage", "python-daemon[build,static-analysis]", "testscenarios (>=0.4)", "testtools"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["docs"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev", "docs"] -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "pyyaml-env-tag" -version = "1.1" -description = "A custom YAML tag for referencing environment variables in YAML files." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, - {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, -] - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "pyzmq" -version = "27.1.0" -description = "Python bindings for 0MQ" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"}, - {file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"}, - {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b"}, - {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e"}, - {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526"}, - {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1"}, - {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386"}, - {file = "pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda"}, - {file = "pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f"}, - {file = "pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32"}, - {file = "pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86"}, - {file = "pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581"}, - {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f"}, - {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e"}, - {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e"}, - {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2"}, - {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394"}, - {file = "pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f"}, - {file = "pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97"}, - {file = "pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07"}, - {file = "pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc"}, - {file = "pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113"}, - {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233"}, - {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31"}, - {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28"}, - {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856"}, - {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496"}, - {file = "pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd"}, - {file = "pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf"}, - {file = "pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f"}, - {file = "pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5"}, - {file = "pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6"}, - {file = "pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7"}, - {file = "pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05"}, - {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9"}, - {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128"}, - {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39"}, - {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97"}, - {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db"}, - {file = "pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c"}, - {file = "pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2"}, - {file = "pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e"}, - {file = "pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a"}, - {file = "pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea"}, - {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96"}, - {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d"}, - {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146"}, - {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd"}, - {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a"}, - {file = "pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92"}, - {file = "pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0"}, - {file = "pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7"}, - {file = "pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b"}, - {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa"}, - {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef"}, - {file = "pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50"}, - {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f"}, - {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0"}, - {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831"}, - {file = "pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312"}, - {file = "pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266"}, - {file = "pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb"}, - {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429"}, - {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d"}, - {file = "pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345"}, - {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968"}, - {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098"}, - {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f"}, - {file = "pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78"}, - {file = "pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db"}, - {file = "pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9"}, - {file = "pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540"}, -] - -[package.dependencies] -cffi = {version = "*", markers = "implementation_name == \"pypy\""} - -[[package]] -name = "radixtarget" -version = "3.0.15" -description = "Check whether an IP address belongs to a cloud provider" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] -files = [ - {file = "radixtarget-3.0.15-py3-none-any.whl", hash = "sha256:1e1d0dd3e8742ffcfc42084eb238f31f6785626b876ab63a9f28a29e97bd3bb0"}, - {file = "radixtarget-3.0.15.tar.gz", hash = "sha256:dedfad3aea1e973f261b7bc0d8936423f59ae4d082648fd496c6cdfdfa069fea"}, -] - -[[package]] -name = "regex" -version = "2026.1.15" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"}, - {file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"}, - {file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"}, - {file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"}, - {file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"}, - {file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"}, - {file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"}, - {file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"}, - {file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"}, - {file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"}, - {file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"}, - {file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"}, - {file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"}, - {file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"}, - {file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"}, - {file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"}, - {file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"}, - {file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"}, - {file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"}, - {file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"}, - {file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"}, - {file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"}, - {file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"}, - {file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"}, - {file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"}, - {file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"}, -] - -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-file" -version = "3.0.1" -description = "File transport adapter for Requests" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2"}, - {file = "requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576"}, -] - -[package.dependencies] -requests = ">=1.0.0" - -[[package]] -name = "resolvelib" -version = "1.0.1" -description = "Resolve abstract dependencies into concrete ones" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf"}, - {file = "resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309"}, -] - -[package.extras] -examples = ["html5lib", "packaging", "pygraphviz", "requests"] -lint = ["black", "flake8", "isort", "mypy", "types-requests"] -release = ["build", "towncrier", "twine"] -test = ["commentjson", "packaging", "pytest"] - -[[package]] -name = "ruff" -version = "0.15.4" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0"}, - {file = "ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992"}, - {file = "ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba"}, - {file = "ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75"}, - {file = "ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac"}, - {file = "ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a"}, - {file = "ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85"}, - {file = "ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db"}, - {file = "ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec"}, - {file = "ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f"}, - {file = "ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338"}, - {file = "ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc"}, - {file = "ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68"}, - {file = "ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3"}, - {file = "ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22"}, - {file = "ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f"}, - {file = "ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453"}, - {file = "ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1"}, -] - -[[package]] -name = "setproctitle" -version = "1.3.7" -description = "A Python module to customize the process title" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "setproctitle-1.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf555b6299f10a6eb44e4f96d2f5a3884c70ce25dc5c8796aaa2f7b40e72cb1b"}, - {file = "setproctitle-1.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:690b4776f9c15aaf1023bb07d7c5b797681a17af98a4a69e76a1d504e41108b7"}, - {file = "setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c"}, - {file = "setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3"}, - {file = "setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f"}, - {file = "setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64"}, - {file = "setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5"}, - {file = "setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381"}, - {file = "setproctitle-1.3.7-cp310-cp310-win32.whl", hash = "sha256:9e803d1b1e20240a93bac0bc1025363f7f80cb7eab67dfe21efc0686cc59ad7c"}, - {file = "setproctitle-1.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:a97200acc6b64ec4cada52c2ecaf1fba1ef9429ce9c542f8a7db5bcaa9dcbd95"}, - {file = "setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0"}, - {file = "setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4"}, - {file = "setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f"}, - {file = "setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9"}, - {file = "setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba"}, - {file = "setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307"}, - {file = "setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee"}, - {file = "setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1"}, - {file = "setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d"}, - {file = "setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4"}, - {file = "setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e"}, - {file = "setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798"}, - {file = "setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629"}, - {file = "setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1"}, - {file = "setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6"}, - {file = "setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c"}, - {file = "setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a"}, - {file = "setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739"}, - {file = "setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f"}, - {file = "setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300"}, - {file = "setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922"}, - {file = "setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee"}, - {file = "setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd"}, - {file = "setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0"}, - {file = "setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929"}, - {file = "setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f"}, - {file = "setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698"}, - {file = "setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c"}, - {file = "setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd"}, - {file = "setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f"}, - {file = "setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9"}, - {file = "setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5"}, - {file = "setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29"}, - {file = "setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152"}, - {file = "setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c"}, - {file = "setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b"}, - {file = "setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18"}, - {file = "setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c"}, - {file = "setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29"}, - {file = "setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9"}, - {file = "setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63"}, - {file = "setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e"}, - {file = "setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f"}, - {file = "setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5"}, - {file = "setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17"}, - {file = "setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e"}, - {file = "setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0"}, - {file = "setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8"}, - {file = "setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed"}, - {file = "setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416"}, - {file = "setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3"}, - {file = "setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309"}, - {file = "setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b"}, - {file = "setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45"}, - {file = "setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4"}, - {file = "setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1"}, - {file = "setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070"}, - {file = "setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73"}, - {file = "setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2"}, - {file = "setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a"}, - {file = "setproctitle-1.3.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:376761125ab5dab822d40eaa7d9b7e876627ecd41de8fa5336713b611b47ccef"}, - {file = "setproctitle-1.3.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a4e03bd9aa5d10b8702f00ec1b740691da96b5003432f3000d60c56f1c2b4d3"}, - {file = "setproctitle-1.3.7-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:47d36e418ab86b3bc7946e27155e281a743274d02cd7e545f5d628a2875d32f9"}, - {file = "setproctitle-1.3.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a74714ce836914063c36c8a26ae11383cf8a379698c989fe46883e38a8faa5be"}, - {file = "setproctitle-1.3.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2ae6c3f042fc866cc0fa2bc35ae00d334a9fa56c9d28dfc47d1b4f5ed23e375"}, - {file = "setproctitle-1.3.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:be7e01f3ad8d0e43954bebdb3088cb466633c2f4acdd88647e7fbfcfe9b9729f"}, - {file = "setproctitle-1.3.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:35a2cabcfdea4643d7811cfe9f3d92366d282b38ef5e7e93e25dafb6f97b0a59"}, - {file = "setproctitle-1.3.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8ce2e39a40fca82744883834683d833e0eb28623752cc1c21c2ec8f06a890b39"}, - {file = "setproctitle-1.3.7-cp38-cp38-win32.whl", hash = "sha256:6f1be447456fe1e16c92f5fb479404a850d8f4f4ff47192fde14a59b0bae6a0a"}, - {file = "setproctitle-1.3.7-cp38-cp38-win_amd64.whl", hash = "sha256:5ce2613e1361959bff81317dc30a60adb29d8132b6159608a783878fc4bc4bbc"}, - {file = "setproctitle-1.3.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:deda9d79d1eb37b688729cac2dba0c137e992ebea960eadb7c2c255524c869e0"}, - {file = "setproctitle-1.3.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a93e4770ac22794cfa651ee53f092d7de7105c76b9fc088bb81ca0dcf698f704"}, - {file = "setproctitle-1.3.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:134e7f66703a1d92c0a9a0a417c580f2cc04b93d31d3fc0dd43c3aa194b706e1"}, - {file = "setproctitle-1.3.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9796732a040f617fc933f9531c9a84bb73c5c27b8074abbe52907076e804b2b7"}, - {file = "setproctitle-1.3.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff3c1c32382fb71a200db8bab3df22f32e6ac7ec3170e92fa5b542cf42eed9a2"}, - {file = "setproctitle-1.3.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:01f27b5b72505b304152cb0bd7ff410cc4f2d69ac70c21a7fdfa64400a68642d"}, - {file = "setproctitle-1.3.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:80b6a562cbc92b289c28f34ce709a16b26b1696e9b9a0542a675ce3a788bdf3f"}, - {file = "setproctitle-1.3.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c4fb90174d176473122e7eef7c6492d53761826f34ff61c81a1c1d66905025d3"}, - {file = "setproctitle-1.3.7-cp39-cp39-win32.whl", hash = "sha256:c77b3f58a35f20363f6e0a1219b367fbf7e2d2efe3d2c32e1f796447e6061c10"}, - {file = "setproctitle-1.3.7-cp39-cp39-win_amd64.whl", hash = "sha256:318ddcf88dafddf33039ad41bc933e1c49b4cb196fe1731a209b753909591680"}, - {file = "setproctitle-1.3.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb440c5644a448e6203935ed60466ec8d0df7278cd22dc6cf782d07911bcbea6"}, - {file = "setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2"}, - {file = "setproctitle-1.3.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6f268caeabb37ccd824d749e7ce0ec6337c4ed954adba33ec0d90cc46b0ab78"}, - {file = "setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1"}, - {file = "setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65"}, - {file = "setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a"}, - {file = "setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e"}, -] - -[package.extras] -test = ["pytest"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["docs"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "socksio" -version = "1.0.0" -description = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, - {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, -] - -[[package]] -name = "soupsieve" -version = "2.8.3" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"}, - {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, -] - -[[package]] -name = "starlette" -version = "0.49.3" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f"}, - {file = "starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284"}, -] - -[package.dependencies] -anyio = ">=3.6.2,<5" -typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} - -[package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] - -[[package]] -name = "starlette" -version = "0.52.1" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74"}, - {file = "starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933"}, -] - -[package.dependencies] -anyio = ">=3.6.2,<5" -typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} - -[package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] - -[[package]] -name = "tabulate" -version = "0.8.10" -description = "Pretty-print tabular data" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] -files = [ - {file = "tabulate-0.8.10-py3-none-any.whl", hash = "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc"}, - {file = "tabulate-0.8.10.tar.gz", hash = "sha256:6c57f3f3dd7ac2782770155f3adb2db0b1a269637e42f27599925e64b114f519"}, -] - -[package.extras] -widechars = ["wcwidth"] - -[[package]] -name = "tldextract" -version = "5.3.0" -description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tldextract-5.3.0-py3-none-any.whl", hash = "sha256:f70f31d10b55c83993f55e91ecb7c5d84532a8972f22ec578ecfbe5ea2292db2"}, - {file = "tldextract-5.3.0.tar.gz", hash = "sha256:b3d2b70a1594a0ecfa6967d57251527d58e00bb5a91a74387baa0d87a0678609"}, -] - -[package.dependencies] -filelock = ">=3.0.8" -idna = "*" -requests = ">=2.1.0" -requests-file = ">=1.4" - -[package.extras] -release = ["build", "twine"] -testing = ["mypy", "pytest", "pytest-gitignore", "pytest-mock", "responses", "ruff", "syrupy", "tox", "tox-uv", "types-filelock", "types-requests"] - -[[package]] -name = "tomli" -version = "2.4.0" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_full_version <= \"3.11.0a6\"" -files = [ - {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, - {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, - {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, - {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, - {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, - {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, - {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, - {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, - {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, - {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, - {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, - {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, - {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, - {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, - {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, - {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, - {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, - {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, - {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, -] - -[[package]] -name = "tomlkit" -version = "0.14.0" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, - {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, -] - -[[package]] -name = "tornado" -version = "6.5.4" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9"}, - {file = "tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843"}, - {file = "tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17"}, - {file = "tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335"}, - {file = "tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f"}, - {file = "tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84"}, - {file = "tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f"}, - {file = "tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8"}, - {file = "tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1"}, - {file = "tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc"}, - {file = "tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1"}, - {file = "tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7"}, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev", "docs"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "unidecode" -version = "1.4.0" -description = "ASCII transliterations of Unicode text" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021"}, - {file = "Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23"}, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev", "docs"] -files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[[package]] -name = "uvicorn" -version = "0.39.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "uvicorn-0.39.0-py3-none-any.whl", hash = "sha256:7beec21bd2693562b386285b188a7963b06853c0d006302b3e4cfed950c9929a"}, - {file = "uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302"}, -] - -[package.dependencies] -click = ">=7.0" -h11 = ">=0.8" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "verspec" -version = "0.1.0" -description = "Flexible version handling" -optional = false -python-versions = "*" -groups = ["docs"] -files = [ - {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, - {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, -] - -[package.extras] -test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] - -[[package]] -name = "virtualenv" -version = "20.36.1" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"}, - {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = [ - {version = ">=3.16.1,<4", markers = "python_version < \"3.10\""}, - {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""}, -] -platformdirs = ">=3.9.1,<5" -typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] - -[[package]] -name = "watchdog" -version = "6.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, - {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, - {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, - {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, - {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, -] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "websockets" -version = "15.0.1" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, - {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, - {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, - {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, - {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, - {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, - {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, - {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, - {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, - {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, - {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, - {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, - {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, -] - -[[package]] -name = "werkzeug" -version = "3.1.6" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131"}, - {file = "werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25"}, -] - -[package.dependencies] -markupsafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog (>=2.3)"] - -[[package]] -name = "wordninja" -version = "2.0.0" -description = "Probabilistically split concatenated words using NLP based on English Wikipedia uni-gram frequencies." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "wordninja-2.0.0.tar.gz", hash = "sha256:1a1cc7ec146ad19d6f71941ee82aef3d31221700f0d8bf844136cf8df79d281a"}, -] - -[[package]] -name = "xmltodict" -version = "0.14.2" -description = "Makes working with XML feel like you are working with JSON" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac"}, - {file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"}, -] - -[[package]] -name = "xmltojson" -version = "2.0.3" -description = "A Python module and cli tool to quickly convert xml text or files into json" -optional = false -python-versions = "<4.0,>=3.7" -groups = ["main"] -files = [ - {file = "xmltojson-2.0.3-py3-none-any.whl", hash = "sha256:1b68519bd14fbf3e28baa630b8c9116b5d3aa8976648f277a78ae3448498889a"}, - {file = "xmltojson-2.0.3.tar.gz", hash = "sha256:68a0022272adf70b8f2639186172c808e9502cd03c0b851a65e0760561c7801d"}, -] - -[package.dependencies] -xmltodict = "0.14.2" - -[[package]] -name = "xxhash" -version = "3.6.0" -description = "Python binding for xxHash" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71"}, - {file = "xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b"}, - {file = "xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b"}, - {file = "xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb"}, - {file = "xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d"}, - {file = "xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a"}, - {file = "xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3"}, - {file = "xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd"}, - {file = "xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef"}, - {file = "xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7"}, - {file = "xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c"}, - {file = "xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae"}, - {file = "xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb"}, - {file = "xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c"}, - {file = "xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829"}, - {file = "xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec"}, - {file = "xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd"}, - {file = "xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799"}, - {file = "xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392"}, - {file = "xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6"}, - {file = "xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702"}, - {file = "xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033"}, - {file = "xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec"}, - {file = "xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8"}, - {file = "xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746"}, - {file = "xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e"}, - {file = "xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5"}, - {file = "xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f"}, - {file = "xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad"}, - {file = "xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679"}, - {file = "xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4"}, - {file = "xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518"}, - {file = "xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119"}, - {file = "xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f"}, - {file = "xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95"}, - {file = "xxhash-3.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7dac94fad14a3d1c92affb661021e1d5cbcf3876be5f5b4d90730775ccb7ac41"}, - {file = "xxhash-3.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6965e0e90f1f0e6cb78da568c13d4a348eeb7f40acfd6d43690a666a459458b8"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2ab89a6b80f22214b43d98693c30da66af910c04f9858dd39c8e570749593d7e"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4903530e866b7a9c1eadfd3fa2fbe1b97d3aed4739a80abf506eb9318561c850"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4da8168ae52c01ac64c511d6f4a709479da8b7a4a1d7621ed51652f93747dffa"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97460eec202017f719e839a0d3551fbc0b2fcc9c6c6ffaa5af85bbd5de432788"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45aae0c9df92e7fa46fbb738737324a563c727990755ec1965a6a339ea10a1df"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d50101e57aad86f4344ca9b32d091a2135a9d0a4396f19133426c88025b09f1"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9085e798c163ce310d91f8aa6b325dda3c2944c93c6ce1edb314030d4167cc65"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:a87f271a33fad0e5bf3be282be55d78df3a45ae457950deb5241998790326f87"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:9e040d3e762f84500961791fa3709ffa4784d4dcd7690afc655c095e02fff05f"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b0359391c3dad6de872fefb0cf5b69d55b0655c55ee78b1bb7a568979b2ce96b"}, - {file = "xxhash-3.6.0-cp38-cp38-win32.whl", hash = "sha256:e4ff728a2894e7f436b9e94c667b0f426b9c74b71f900cf37d5468c6b5da0536"}, - {file = "xxhash-3.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:01be0c5b500c5362871fc9cfdf58c69b3e5c4f531a82229ddb9eb1eb14138004"}, - {file = "xxhash-3.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc604dc06027dbeb8281aeac5899c35fcfe7c77b25212833709f0bff4ce74d2a"}, - {file = "xxhash-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:277175a73900ad43a8caeb8b99b9604f21fe8d7c842f2f9061a364a7e220ddb7"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfbc5b91397c8c2972fdac13fb3e4ed2f7f8ccac85cd2c644887557780a9b6e2"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2762bfff264c4e73c0e507274b40634ff465e025f0eaf050897e88ec8367575d"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f171a900d59d51511209f7476933c34a0c2c711078d3c80e74e0fe4f38680ec"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:780b90c313348f030b811efc37b0fa1431163cb8db8064cf88a7936b6ce5f222"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b242455eccdfcd1fa4134c431a30737d2b4f045770f8fe84356b3469d4b919"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a75ffc1bd5def584129774c158e108e5d768e10b75813f2b32650bb041066ed6"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1fc1ed882d1e8df932a66e2999429ba6cc4d5172914c904ab193381fba825360"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:44e342e8cc11b4e79dae5c57f2fb6360c3c20cc57d32049af8f567f5b4bcb5f4"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c2f9ccd5c4be370939a2e17602fbc49995299203da72a3429db013d44d590e86"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02ea4cb627c76f48cd9fb37cf7ab22bd51e57e1b519807234b473faebe526796"}, - {file = "xxhash-3.6.0-cp39-cp39-win32.whl", hash = "sha256:6551880383f0e6971dc23e512c9ccc986147ce7bfa1cd2e4b520b876c53e9f3d"}, - {file = "xxhash-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:7c35c4cdc65f2a29f34425c446f2f5cdcd0e3c34158931e1cc927ece925ab802"}, - {file = "xxhash-3.6.0-cp39-cp39-win_arm64.whl", hash = "sha256:ffc578717a347baf25be8397cb10d2528802d24f94cfc005c0e44fef44b5cdd6"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d"}, - {file = "xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6"}, -] - -[[package]] -name = "yara-python" -version = "4.5.2" -description = "Python interface for YARA" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "yara_python-4.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20aee068c8f14e8ebb40ebf03e7e2c14031736fbf6f32fca58ad89d211e4aaa0"}, - {file = "yara_python-4.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9899c3a80e6c543585daf49c5b06ba5987e2f387994a5455d841262ea6e8577c"}, - {file = "yara_python-4.5.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:399bb09f81d38876a06e269f68bbe810349aa0bb47fe79866ea3fc58ce38d30f"}, - {file = "yara_python-4.5.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:c78608c6bf3d2c379514b1c118a104874df1844bf818087e1bf6bfec0edfd1aa"}, - {file = "yara_python-4.5.2-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:f25db30f8ae88a4355e5090a5d6191ee6f2abfdd529b3babc68a1faeba7c2ac8"}, - {file = "yara_python-4.5.2-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:f2866c0b8404086c5acb68cab20854d439009a1b02077aca22913b96138d2f6a"}, - {file = "yara_python-4.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fc5abddf8767ca923a5a88b38b8057d4fab039323d5c6b2b5be6cba5e6e7350"}, - {file = "yara_python-4.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc2216bc73d4918012a4b270a93f9042445c7246b4a668a1bea220fbf64c7990"}, - {file = "yara_python-4.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5558325eb7366f610a06e8c7c4845062d6880ee88f1fbc35e92fae333c3333c"}, - {file = "yara_python-4.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a293e30484abb6c137d9603fe899dfe112c327bf7a75e46f24737dd43a5e44"}, - {file = "yara_python-4.5.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ff1e140529e7ade375b3c4c2623d155c93438bd56c8e9bebce30b1d0831350d"}, - {file = "yara_python-4.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:399f484847d5cb978f3dd522d3c0f20cbf36fe760d90be7aaeb5cf0e82947742"}, - {file = "yara_python-4.5.2-cp310-cp310-win32.whl", hash = "sha256:ef499e273d12b0119fc59b396a85f00d402b103c95b5a4075273cff99f4692df"}, - {file = "yara_python-4.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:dd54d92c8fe33cc7cd7b8b29ac8ac5fdb6ca498c5a697af479ff31a58258f023"}, - {file = "yara_python-4.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:727d3e590f41a89bbc6c1341840a398dee57bc816b9a17f69aed717f79abd5af"}, - {file = "yara_python-4.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5657c268275a025b7b2f2f57ea2be0b7972a104cce901c0ac3713787eea886e"}, - {file = "yara_python-4.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4bcfa3d4bda3c0822871a35dd95acf6a0fe1ab2d7869b5ae25b0a722688053a"}, - {file = "yara_python-4.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d6d7e04d1f5f64ccc7d60ff76ffa5a24d929aa32809f20c2164799b63f46822"}, - {file = "yara_python-4.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d487dcce1e9cf331a707e16a12c841f99071dcd3e17646fff07d8b3da6d9a05c"}, - {file = "yara_python-4.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f8ca11d6877d453f69987b18963398744695841b4e2e56c2f1763002d5d22dbd"}, - {file = "yara_python-4.5.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f1f009d99e05f5be7c3d4e349c949226bfe32e0a9c3c75ff5476e94385824c26"}, - {file = "yara_python-4.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96ead034a1aef94671ea92a82f1c2db6defa224cf21eb5139cff7e7345e55153"}, - {file = "yara_python-4.5.2-cp311-cp311-win32.whl", hash = "sha256:7b19ac28b3b55134ea12f1ee8500d7f695e735e9bead46b822abec96f9587f06"}, - {file = "yara_python-4.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:a699917ea1f3f47aecacd8a10b8ee82137205b69f9f29822f839a0ffda2c41a1"}, - {file = "yara_python-4.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:037be5f9d5dd9f067bbbeeac5d311815611ba8298272a14b03d7ad0f42b36f5a"}, - {file = "yara_python-4.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:77c8192f56e2bbf42b0c16cd1c368ba7083047e5b11467c8b3d6330d268e1f86"}, - {file = "yara_python-4.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e892b2111122552f0645bc1a55f2525117470eea3b791d452de12ae0c1ec37b"}, - {file = "yara_python-4.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f16d9b23f107fd0569c676ec9340b24dd5a2a2a239a163dcdeaed6032933fb94"}, - {file = "yara_python-4.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b98e0a77dc0f90bc53cf77cca1dc1a4e6836c7c5a283198c84d5dbb0701e722"}, - {file = "yara_python-4.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d6366d197079848d4c2534f07bc47f8a3c53d42855e6a492ed2191775e8cd294"}, - {file = "yara_python-4.5.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a2ba9fddafe573614fc8e77973f07e74a359bd1f3a6152f93b814a6f8cfc0004"}, - {file = "yara_python-4.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3338f492e9bb655381dbf7e526201c1331d8c1e3760f1b06f382d901cc10cdf0"}, - {file = "yara_python-4.5.2-cp312-cp312-win32.whl", hash = "sha256:9d066da7f963f4a68a2681cbe1d7c41cb1ef2c5668de3a756731b1a7669a3120"}, - {file = "yara_python-4.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:fe5b4c9c5cb48526e8f9c67fc1fdafb9dbd9078a27d89af30de06424c8c67588"}, - {file = "yara_python-4.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ffc3101354188d23d00b831b0d070e2d1482a60d4e9964452004276f7c1edee8"}, - {file = "yara_python-4.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c7021e6c4e34b2b11ad82de330728134831654ca1f5c24dcf093fedc0db07ae"}, - {file = "yara_python-4.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73009bd6e73b04ffcbc8d47dddd4df87623207cb772492c516e16605ced5dd6"}, - {file = "yara_python-4.5.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef7f592db5e2330efd01b40760c3e2be5de497ff22bd6d12e63e9cf6f37b4213"}, - {file = "yara_python-4.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5980d96ac2742e997e55ba20d2746d3a42298bbb5e7d327562a01bac70c9268"}, - {file = "yara_python-4.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e857bc94ef54d5db89e0a58652df609753d5b95f837dde101e1058dd755896b5"}, - {file = "yara_python-4.5.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98b4732a9f5b184ade78b4675501fbdc4975302dc78aa3e917c60ca4553980d5"}, - {file = "yara_python-4.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57928557c85af27d27cca21de66d2070bf1860de365fb18fc591ddfb1778b959"}, - {file = "yara_python-4.5.2-cp313-cp313-win32.whl", hash = "sha256:d7b58296ed2d262468d58f213b19df3738e48d46b8577485aecca0edf703169f"}, - {file = "yara_python-4.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f6ccde3f30d0c3cda9a86e91f2a74073c9aeb127856d9a62ed5c4bb22ccd75f"}, - {file = "yara_python-4.5.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ba81f3bfba7cd7aa7bf97840eba7e2bb3d9f643090e47cbc03b2070e4f44568f"}, - {file = "yara_python-4.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6cbb9ded3d6dd981cb1f1a7e01b12efd260548bc2f27bf29e9dbeca1ab241363"}, - {file = "yara_python-4.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb760bb5aaa9c37e0e43b64cccfb7ff1a5ae584392661ebd82a50b758ea2d86"}, - {file = "yara_python-4.5.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96b22ae1651f8fd2eb61d0d140daa71dce4346137124abead0bb15c47b1259ec"}, - {file = "yara_python-4.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e96c6cdf2284077ae039832481046806cd2b0c42c45d160da567dd0cc5673f3"}, - {file = "yara_python-4.5.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:475fb16f33767c8c048cf009cdf307688b4ed961cf29fc28b2a020c3469e4cba"}, - {file = "yara_python-4.5.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c195d69253681751d75c6f79c88f57ebf5cc547821bdcba89fa29466356f241b"}, - {file = "yara_python-4.5.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8cc49f5543398b48e6bcf20c6a93a34ebe6732b8ff1e55918c8908a4a6cfeaf8"}, - {file = "yara_python-4.5.2-cp39-cp39-win32.whl", hash = "sha256:2ccd788c8103f43c58d4072f696ee7b7e5be6c19bbce32f9f8e5d7b7def3ecd4"}, - {file = "yara_python-4.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:ab8d75dc181b915ca7eb8eb1c3f66597286436820122f84ae9f07a7b98f256fc"}, - {file = "yara_python-4.5.2.tar.gz", hash = "sha256:9086a53c810c58740a5129f14d126b39b7ef61af00d91580c2efb654e2f742ce"}, -] - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev", "docs"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] -markers = {main = "python_version == \"3.9\"", dev = "python_version == \"3.9\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.1" -python-versions = "^3.9" -content-hash = "ce3d5b9bdb56ed6edad9e4c72f629fe218c383289e75c4613fbffa341dca7e4e" diff --git a/pyproject.toml b/pyproject.toml index 8a79327993..9f14e3f899 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,93 +1,107 @@ -[tool.poetry] +[project] name = "bbot" -version = "2.8.4" +dynamic = ["version"] description = "OSINT automation for hackers." +readme = "README.md" +license = "AGPL-3.0" +requires-python = ">=3.10,<3.15" authors = [ - "TheTechromancer", - "Paul Mueller", + { name = "TheTechromancer" }, + { name = "Paul Mueller" }, ] -license = "GPL-3.0" -readme = "README.md" -repository = "https://github.com/blacklanternsecurity/bbot" -homepage = "https://github.com/blacklanternsecurity/bbot" -documentation = "https://www.blacklanternsecurity.com/bbot/" keywords = ["python", "cli", "automation", "osint", "threat-intel", "intelligence", "neo4j", "scanner", "python-library", "hacking", "recursion", "pentesting", "recon", "command-line-tool", "bugbounty", "subdomains", "security-tools", "subdomain-scanner", "osint-framework", "attack-surface", "subdomain-enumeration", "osint-tool"] classifiers = [ "Operating System :: POSIX :: Linux", "Topic :: Security", ] +dependencies = [ + "pip", + "omegaconf>=2.3.0,<3", + "psutil>=5.9.4,<8.0.0", + "wordninja>=2.0.0,<3", + "ansible-runner>=2.3.2,<3", + "deepdiff>=8.0.0,<9", + "xmltojson>=2.0.2,<3", + "pycryptodome>=3.17,<4", + "idna>=3.4,<4", + "tabulate==0.8.10", + "websockets>=14.0.0,<16.0.0", + "pyjwt>=2.7.0,<3", + "beautifulsoup4>=4.12.2,<5", + "lxml>=4.9.2,<7.0.0", + "dnspython>=2.7.0,<2.8.0", + "cachetools>=5.3.2,<7.0.0", + "socksio>=1.0.0,<2", + "jinja2>=3.1.3,<4", + "regex>=2024.4.16,<2027.0.0", + "unidecode>=1.3.8,<2", + "mmh3>=4.1,<6.0", + "xxhash>=3.5.0,<4", + "setproctitle>=1.3.3,<2", + "yara-python==4.5.2", + "pyzmq>=26.0.3,<28.0.0", + "httpx>=0.28.1,<1", + "puremagic>=1.28,<2", + "pydantic>=2.12.2,<3", + "radixtarget>=4.0.1,<5", + "asndb>=1.0.4", + "orjson>=3.10.12,<4", + "ansible-core>=2.17,<3", + "tldextract>=5.3.0,<6", + "cloudcheck>=9.2.0,<10", +] -[tool.poetry.urls] -"Discord" = "https://discord.com/invite/PZqkgxu5SA" +[project.urls] +Repository = "https://github.com/blacklanternsecurity/bbot" +Homepage = "https://github.com/blacklanternsecurity/bbot" +Documentation = "https://www.blacklanternsecurity.com/bbot/" +Discord = "https://discord.com/invite/PZqkgxu5SA" "Docker Hub" = "https://hub.docker.com/r/blacklanternsecurity/bbot" -[tool.poetry.scripts] -bbot = 'bbot.cli:main' +[project.scripts] +bbot = "bbot.cli:main" -[tool.poetry.dependencies] -python = "^3.9" -omegaconf = "^2.3.0" -psutil = ">=5.9.4,<8.0.0" -wordninja = "^2.0.0" -ansible-runner = "^2.3.2" -deepdiff = "^8.0.0" -xmltojson = "^2.0.2" -pycryptodome = "^3.17" -idna = "^3.4" -tabulate = "0.8.10" -websockets = ">=14.0.0,<16.0.0" -pyjwt = "^2.7.0" -beautifulsoup4 = "^4.12.2" -lxml = ">=4.9.2,<7.0.0" -dnspython = ">=2.7.0,<2.8.0" -cachetools = ">=5.3.2,<7.0.0" -socksio = "^1.0.0" -jinja2 = "^3.1.3" -regex = ">=2024.4.16,<2027.0.0" -unidecode = "^1.3.8" -mmh3 = ">=4.1,<6.0" -xxhash = "^3.5.0" -setproctitle = "^1.3.3" -yara-python = "4.5.2" -pyzmq = ">=26.0.3,<28.0.0" -httpx = "^0.28.1" -puremagic = "^1.28" -pydantic = "^2.9.2" -radixtarget = "^3.0.13" -orjson = "^3.10.12" -ansible-core = "^2.15.13" -tldextract = "^5.3.0" -cloudcheck = "^9.2.0" +[dependency-groups] +dev = [ + "urllib3>=2.0.2,<3", + "werkzeug>=2.3.4,<4.0.0", + "pytest-env>=0.8.2,<1.2.0", + "pre-commit>=3.4,<5.0", + "pytest-cov>=5,<8", + "pytest-rerunfailures>=14,<17", + "pytest-timeout>=2.3.1,<3", + "pytest-httpserver>=1.0.11,<2", + "pytest>=8.3.1,<9", + "pytest-asyncio==1.2.0", + "uvicorn>=0.32,<0.40", + "fastapi>=0.115.5,<0.129.0", + "pytest-httpx>=0.35", + "pytest-benchmark>=4,<6", + "ruff==0.15.2", + "baddns~=2.0.0", +] +docs = [ + "mkdocs>=1.5.2,<2", + "mkdocs-extra-sass-plugin>=0.1.0,<1", + "mkdocs-material>=9.2.5,<10", + "mkdocs-material-extensions>=1.1.1,<2", + "mkdocstrings>=0.22,<0.31", + "mkdocstrings-python>=2.0.0,<3", + "griffe>=1,<2", + "livereload>=2.6.3,<3", + "mike>=2.1.3,<3", + "pymdown-extensions>=10.20.1,<11", +] -[tool.poetry.group.dev.dependencies] -poetry-dynamic-versioning = ">=0.21.4,<1.11.0" -urllib3 = "^2.0.2" -werkzeug = ">=2.3.4,<4.0.0" -pytest-env = ">=0.8.2,<1.2.0" -pre-commit = ">=3.4,<5.0" -pytest-cov = ">=5,<8" -pytest-rerunfailures = ">=14,<17" -pytest-timeout = "^2.3.1" -pytest-httpserver = "^1.0.11" -pytest = "^8.3.1" -pytest-asyncio = "1.2.0" -uvicorn = ">=0.32,<0.40" -fastapi = ">=0.115.5,<0.129.0" -pytest-httpx = ">=0.35" -pytest-benchmark = ">=4,<6" -ruff = "0.15.4" -pymdown-extensions = "^10.20.1" -griffe = "^1" +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry.group.docs.dependencies] -mkdocs = "^1.5.2" -mkdocs-extra-sass-plugin = "^0.1.0" -mkdocs-material = "^9.2.5" -mkdocs-material-extensions = "^1.1.1" -mkdocstrings = ">=0.22,<0.31" -mkdocstrings-python = "^1.6.0" -livereload = "^2.6.3" -mike = "^2.1.3" +[tool.hatch.version] +path = "bbot/_version.py" + +[tool.hatch.build.targets.wheel] +packages = ["bbot"] [tool.pytest.ini_options] env = [ @@ -106,10 +120,6 @@ warmup_iterations = 3 disable_gc = true min_rounds = 5 -[build-system] -requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"] -build-backend = "poetry_dynamic_versioning.backend" - [tool.codespell] ignore-words-list = "bu,cna,couldn,dialin,nd,ned,thirdparty" skip = "./docs/javascripts/vega*.js,./bbot/wordlists/*" @@ -119,11 +129,3 @@ line-length = 119 format.exclude = ["bbot/test/test_step_1/test_manager_*"] lint.select = ["E", "F"] lint.ignore = ["E402", "E711", "E713", "E721", "E741", "F403", "F405", "E501"] - -[tool.poetry-dynamic-versioning] -enable = true -metadata = false -format-jinja = 'v2.8.4{% if branch == "dev" %}.{{ distance }}rc{% endif %}' - -[tool.poetry-dynamic-versioning.substitution] -files = ["*/__init__.py"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..ae7c73d099 --- /dev/null +++ b/uv.lock @@ -0,0 +1,3273 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.15" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "ansible-core" +version = "2.17.14" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "cryptography", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pyyaml", marker = "python_full_version < '3.11'" }, + { name = "resolvelib", version = "1.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/80/2925a0564f6f99a8002c3be3885b83c3a1dc5f57ebf00163f528889865f5/ansible_core-2.17.14.tar.gz", hash = "sha256:7c17fee39f8c29d70e3282a7e9c10bd70d5cd4fd13ddffc5dcaa52adbd142ff8", size = 3119687, upload-time = "2025-09-08T18:28:03.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/29/d694562f1a875b50aa74f691521fe493704f79cf1938cd58f28f7e2327d2/ansible_core-2.17.14-py3-none-any.whl", hash = "sha256:34a49582a57c2f2af17ede2cefd3b3602a2d55d22089f3928570d52030cafa35", size = 2189656, upload-time = "2025-09-08T18:28:00.375Z" }, +] + +[[package]] +name = "ansible-core" +version = "2.19.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "cryptography", marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pyyaml", marker = "python_full_version == '3.11.*'" }, + { name = "resolvelib", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/98/ca1135c50aa3004367ff1f66c101a565ac17b4bfa1c9f9a2d8e395e06775/ansible_core-2.19.6.tar.gz", hash = "sha256:1e6b711e357901422592a1d1e4a076eee918497587646a5843fa61536ede1990", size = 3414480, upload-time = "2026-01-29T19:24:10.09Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/2e/5127c0321b7fad3e636a3b2343e711b64bdb77f25a6de0d579268f8b77cc/ansible_core-2.19.6-py3-none-any.whl", hash = "sha256:a29c5df4b46cc3f4123e5aac15f3626b925841b9844fa88d3890a0c45a9a4469", size = 2415790, upload-time = "2026-01-29T19:24:07.595Z" }, +] + +[[package]] +name = "ansible-core" +version = "2.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pyyaml", marker = "python_full_version >= '3.12'" }, + { name = "resolvelib", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/56/a76adc20dee854b52a3e20fcb9c01280bbac52ef54308e5b1c7bc67ade76/ansible_core-2.20.2.tar.gz", hash = "sha256:75e19a3ad8cf659579ea182cdf948ee0900d700e564802e92876de53dbd9715d", size = 3317427, upload-time = "2026-01-29T19:25:04.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/89/9d0d5ce1e63e57d59ae1c873a2c2b08ab104bd3bea365db46c3140371271/ansible_core-2.20.2-py3-none-any.whl", hash = "sha256:1bbd101e3e3b1ace91d8be123007050f7efd94c4c78bbeb9e45ad1c7016d08ef", size = 2412886, upload-time = "2026-01-29T19:25:03.04Z" }, +] + +[[package]] +name = "ansible-runner" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pexpect" }, + { name = "python-daemon" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/db/65b9e058807d313c495a6f4365cc11234d0391c5843659ddc27cc4bf1677/ansible_runner-2.4.2.tar.gz", hash = "sha256:331d4da8d784e5a76aa9356981c0255f4bb1ba640736efe84b0bd7c73a4ca420", size = 152047, upload-time = "2025-10-14T19:10:50.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/da/19512e72e9cf2b8e7e6345264baa6c7ac1bb0ab128eb19c73a58407c4566/ansible_runner-2.4.2-py3-none-any.whl", hash = "sha256:0bde6cb39224770ff49ccdc6027288f6a98f4ed2ea0c64688b31217033221893", size = 79758, upload-time = "2025-10-14T19:10:48.994Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "asndb" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "httpx" }, + { name = "radixtarget" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/2b/86f8d056d24476aeb0f7e63fb971b870f7018e728d9272a05232157cae30/asndb-1.0.4.tar.gz", hash = "sha256:23989a4a09e66b76a86b1a36eea78138b4a3001b5f909bb89e110635bb1723f2", size = 18956, upload-time = "2026-03-27T20:45:56.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d7/a9084a90ea60919f84c39dc2302d802c9047b87373fcc2309898ca4095d9/asndb-1.0.4-py3-none-any.whl", hash = "sha256:a62e4f5872e12cbd4b146c7583f7bf2a5239e43ed20a63728295058ee3f1a3db", size = 18467, upload-time = "2026-03-27T20:45:57.022Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "backrefs" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, +] + +[[package]] +name = "baddns" +version = "2.0.452" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudcheck" }, + { name = "colorama" }, + { name = "dnspython" }, + { name = "httpx" }, + { name = "python-dateutil" }, + { name = "python-whois" }, + { name = "pyyaml" }, + { name = "tldextract" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/32/1d82b8781bd9285fabb4a03f342bfc8e27f219665af5dd184fc97bf9694a/baddns-2.0.452.tar.gz", hash = "sha256:193e3ff866986ddb626ec563991c3ea760985ca74f7ed59c0f3770c9f5543d9d", size = 61143, upload-time = "2026-03-20T18:39:14.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/cb/7ed49be7f8802adfcb57710548ed28239d899eaf5db7a49e57df8600385f/baddns-2.0.452-py3-none-any.whl", hash = "sha256:54e38e2b661de981f95454abea8b17a6bb3d1d5ff1e046738c319cfd7cfc018d", size = 117120, upload-time = "2026-03-20T18:39:13.376Z" }, +] + +[[package]] +name = "bbot" +source = { editable = "." } +dependencies = [ + { name = "ansible-core", version = "2.17.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ansible-core", version = "2.19.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "ansible-core", version = "2.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "ansible-runner" }, + { name = "asndb" }, + { name = "beautifulsoup4" }, + { name = "cachetools" }, + { name = "cloudcheck" }, + { name = "deepdiff" }, + { name = "dnspython" }, + { name = "httpx" }, + { name = "idna" }, + { name = "jinja2" }, + { name = "lxml" }, + { name = "mmh3" }, + { name = "omegaconf" }, + { name = "orjson" }, + { name = "pip" }, + { name = "psutil" }, + { name = "puremagic" }, + { name = "pycryptodome" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "pyzmq" }, + { name = "radixtarget" }, + { name = "regex" }, + { name = "setproctitle" }, + { name = "socksio" }, + { name = "tabulate" }, + { name = "tldextract" }, + { name = "unidecode" }, + { name = "websockets" }, + { name = "wordninja" }, + { name = "xmltojson" }, + { name = "xxhash" }, + { name = "yara-python" }, +] + +[package.dev-dependencies] +dev = [ + { name = "baddns" }, + { name = "fastapi" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-benchmark" }, + { name = "pytest-cov" }, + { name = "pytest-env" }, + { name = "pytest-httpserver" }, + { name = "pytest-httpx" }, + { name = "pytest-rerunfailures" }, + { name = "pytest-timeout" }, + { name = "ruff" }, + { name = "urllib3" }, + { name = "uvicorn" }, + { name = "werkzeug" }, +] +docs = [ + { name = "griffe" }, + { name = "livereload" }, + { name = "mike" }, + { name = "mkdocs" }, + { name = "mkdocs-extra-sass-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-material-extensions" }, + { name = "mkdocstrings" }, + { name = "mkdocstrings-python" }, + { name = "pymdown-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "ansible-core", specifier = ">=2.17,<3" }, + { name = "ansible-runner", specifier = ">=2.3.2,<3" }, + { name = "asndb", specifier = ">=1.0.4" }, + { name = "beautifulsoup4", specifier = ">=4.12.2,<5" }, + { name = "cachetools", specifier = ">=5.3.2,<7.0.0" }, + { name = "cloudcheck", specifier = ">=9.2.0,<10" }, + { name = "deepdiff", specifier = ">=8.0.0,<9" }, + { name = "dnspython", specifier = ">=2.7.0,<2.8.0" }, + { name = "httpx", specifier = ">=0.28.1,<1" }, + { name = "idna", specifier = ">=3.4,<4" }, + { name = "jinja2", specifier = ">=3.1.3,<4" }, + { name = "lxml", specifier = ">=4.9.2,<7.0.0" }, + { name = "mmh3", specifier = ">=4.1,<6.0" }, + { name = "omegaconf", specifier = ">=2.3.0,<3" }, + { name = "orjson", specifier = ">=3.10.12,<4" }, + { name = "pip" }, + { name = "psutil", specifier = ">=5.9.4,<8.0.0" }, + { name = "puremagic", specifier = ">=1.28,<2" }, + { name = "pycryptodome", specifier = ">=3.17,<4" }, + { name = "pydantic", specifier = ">=2.12.2,<3" }, + { name = "pyjwt", specifier = ">=2.7.0,<3" }, + { name = "pyzmq", specifier = ">=26.0.3,<28.0.0" }, + { name = "radixtarget", specifier = ">=4.0.1,<5" }, + { name = "regex", specifier = ">=2024.4.16,<2027.0.0" }, + { name = "setproctitle", specifier = ">=1.3.3,<2" }, + { name = "socksio", specifier = ">=1.0.0,<2" }, + { name = "tabulate", specifier = "==0.8.10" }, + { name = "tldextract", specifier = ">=5.3.0,<6" }, + { name = "unidecode", specifier = ">=1.3.8,<2" }, + { name = "websockets", specifier = ">=14.0.0,<16.0.0" }, + { name = "wordninja", specifier = ">=2.0.0,<3" }, + { name = "xmltojson", specifier = ">=2.0.2,<3" }, + { name = "xxhash", specifier = ">=3.5.0,<4" }, + { name = "yara-python", specifier = "==4.5.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "baddns", specifier = "~=2.0.0" }, + { name = "fastapi", specifier = ">=0.115.5,<0.129.0" }, + { name = "pre-commit", specifier = ">=3.4,<5.0" }, + { name = "pytest", specifier = ">=8.3.1,<9" }, + { name = "pytest-asyncio", specifier = "==1.2.0" }, + { name = "pytest-benchmark", specifier = ">=4,<6" }, + { name = "pytest-cov", specifier = ">=5,<8" }, + { name = "pytest-env", specifier = ">=0.8.2,<1.2.0" }, + { name = "pytest-httpserver", specifier = ">=1.0.11,<2" }, + { name = "pytest-httpx", specifier = ">=0.35" }, + { name = "pytest-rerunfailures", specifier = ">=14,<17" }, + { name = "pytest-timeout", specifier = ">=2.3.1,<3" }, + { name = "ruff", specifier = "==0.15.2" }, + { name = "urllib3", specifier = ">=2.0.2,<3" }, + { name = "uvicorn", specifier = ">=0.32,<0.40" }, + { name = "werkzeug", specifier = ">=2.3.4,<4.0.0" }, +] +docs = [ + { name = "griffe", specifier = ">=1,<2" }, + { name = "livereload", specifier = ">=2.6.3,<3" }, + { name = "mike", specifier = ">=2.1.3,<3" }, + { name = "mkdocs", specifier = ">=1.5.2,<2" }, + { name = "mkdocs-extra-sass-plugin", specifier = ">=0.1.0,<1" }, + { name = "mkdocs-material", specifier = ">=9.2.5,<10" }, + { name = "mkdocs-material-extensions", specifier = ">=1.1.1,<2" }, + { name = "mkdocstrings", specifier = ">=0.22,<0.31" }, + { name = "mkdocstrings-python", specifier = ">=2.0.0,<3" }, + { name = "pymdown-extensions", specifier = ">=10.20.1,<11" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudcheck" +version = "9.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/44eaf6fedaa57acc2c1581080366ef14c850942dd134e89233cc86fbae2e/cloudcheck-9.3.0.tar.gz", hash = "sha256:e4f92690f84b176395d01a0694263d8edb0f8fd3a63100757376b7810879e6f5", size = 4428042, upload-time = "2026-02-03T17:19:12.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/9e/84925a7ab1041bb7d2d26ce53fad2b289cec2d2533f0fd58be2c1ee0b43e/cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59199ed17b14ca87220ad4b13ca38999a36826a63fc3a86f6274289c3247bddb", size = 4168371, upload-time = "2026-02-03T17:34:56.274Z" }, + { url = "https://files.pythonhosted.org/packages/19/88/33cf4ec8c27c482ee5513f415d559d98db6bc8df3016281164bee620aa35/cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e9f3b13eafafde34be9f1ca2aca897f6bbaf955c04144e42c3877228b3569f3", size = 3521015, upload-time = "2026-02-03T17:35:12.938Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/eb2ef322dd900e7aed339cad46ca36c70037561801adc211f1848eadb13e/cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e6aeea4742501dde2b7815877a925da0b1463e51ebae819b5868f46ceb68024", size = 4115104, upload-time = "2026-02-03T17:35:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f8/3f9c55449d2fa7d349081e68b77dc42422671350af6a1dd4bee184accaa9/cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7bd368a8417e67a7313f276429d1fcf3f4fb2ee6604e4e708ac65112f22aac5", size = 4036731, upload-time = "2026-02-03T17:35:29.164Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ec/fa76803f7d705d1691ec746161ef7f92209c10ab183cbe313222505ba023/cloudcheck-9.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9722d5dafcbb56152c0fd32d19573e5dd91d6f6d07981d0ef0fca9ae47900eb", size = 3966898, upload-time = "2026-02-03T17:35:56.741Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c0/45449a38333e2049e925d7ea44306350a25c99b77edc5d6d449efcf99ae0/cloudcheck-9.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b83396008638a6efd631b25b435f31b758732fae97beb5fef5fa1997619ede0d", size = 4564839, upload-time = "2026-02-03T17:36:22.083Z" }, + { url = "https://files.pythonhosted.org/packages/bb/68/d98f3eb20c69dd27636fc7f00d4095600637e434e64263f936eb64dfbafc/cloudcheck-9.3.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:43d38b7195929e19287bf7e9c0155b8dd3cafaebddc642d31b96629c05d775c0", size = 3849723, upload-time = "2026-02-03T17:36:37.379Z" }, + { url = "https://files.pythonhosted.org/packages/06/85/6423089eed890c6cd0c6ff6006aef64e4a41bd8b36e415165c5b8b6eeb2c/cloudcheck-9.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ee2c52294285087b5f65715cdd8fc97358cce25af88ed265c1a39c9ac407cb2c", size = 4206075, upload-time = "2026-02-03T17:36:51.806Z" }, + { url = "https://files.pythonhosted.org/packages/56/48/3737364dc9c01e9994cf4fbdda90e106578659be23be173c96dd1e3c69c5/cloudcheck-9.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:07e8dba045fc365f316849d4caac8c06886c5eb602fc9239067822c0ef6a8737", size = 4230526, upload-time = "2026-02-03T17:37:08.658Z" }, + { url = "https://files.pythonhosted.org/packages/f1/00/c6231b08fe1cf3f4ecab417b56d3f101481a8c767ff8e2f11b639499661b/cloudcheck-9.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:3b88fb61d8242ef1801d61177849a168a6427b4b113e5d2f4787c428a862a113", size = 1400556, upload-time = "2026-02-03T17:37:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5c/29a00dc2aff7816bd2a570562f7ba5b10ad8c3ff83cdb629f07eb34fec5a/cloudcheck-9.3.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e765635136318808997deb3e633a35cde914479003321de21654a0f1b03b8820", size = 1626556, upload-time = "2026-02-03T17:36:14.845Z" }, + { url = "https://files.pythonhosted.org/packages/f2/19/31714dae275f5bab8e3101e9cd6e7f2c2c200271395c75b699e835bd42ac/cloudcheck-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e275ee18f4991e50476971de5986fe42dc9180e66fd04f853d1c1adf4457379b", size = 1592390, upload-time = "2026-02-03T17:36:08.159Z" }, + { url = "https://files.pythonhosted.org/packages/ca/93/13e9f3a8c26eb3e88414943b9fc56b6e8441a7b838de6a35db663673f209/cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e8b26d6f57c8c4a95491235ebe31ece0d24c33c18e1226293cc47437b6b4d3", size = 4169674, upload-time = "2026-02-03T17:34:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/7731c84358f6d91b4d8f672171dba0d2cc59652df04659b1cb5b47a1078d/cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1f61946050445be504dd9a2875fc15109d24d99f79b8925b2a8babaa62079ca2", size = 3520855, upload-time = "2026-02-03T17:35:15.657Z" }, + { url = "https://files.pythonhosted.org/packages/07/fe/0745a67fa7c26da9f8a0e366e8800291337ddd3ccb64773daeb210e8e514/cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2f08ad1719d485d4049c6ad4c2867b979f9f2d8002459baf7b6f8e364ec6b78", size = 4116541, upload-time = "2026-02-03T17:35:46.896Z" }, + { url = "https://files.pythonhosted.org/packages/36/40/abc5077924e0500df40d5b61ce913c66c3a9304cda623c95d46764d155d4/cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bc6c167bb0be90933f0c5907a4d3a82d23a02bb71aaab378fd8d6b76eac585", size = 4036986, upload-time = "2026-02-03T17:35:30.741Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8e/2982a055c4daff6b5c898982dede9d4ff18ca9a5392257ae96b2f36a7b1e/cloudcheck-9.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5322e9aaf54e9d664436a305067976b7c1cff50a7dd2648f593bb4f02bfea9a", size = 3966463, upload-time = "2026-02-03T17:35:58.182Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/6afdff8c897642592fdd628b86a15a0f67d0da28b2f2da9088c4ba5e118c/cloudcheck-9.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9be898d7a98105f25e292c6f958ad862c5915c95c1628dc6dcdf7c9f9db404fd", size = 4565066, upload-time = "2026-02-03T17:36:23.716Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a1/a364abfcfb7498885a6d2ed0f802d93c636a5ebd4e7fbac3b579e8824ff1/cloudcheck-9.3.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d3ee8c28efc9fc69122cfbec0b1dfc72469d905227f4cccaee490b8c725b88", size = 3849502, upload-time = "2026-02-03T17:36:38.992Z" }, + { url = "https://files.pythonhosted.org/packages/50/a4/6dd97aaeeb9d1e9b18253e895d6888274a0b65b755616c7106bce9b54c5d/cloudcheck-9.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:becc2a61d07b8364280f33fc5540ddaf6c9d96f50ac5b1de0922036a76c685af", size = 4207029, upload-time = "2026-02-03T17:36:53.705Z" }, + { url = "https://files.pythonhosted.org/packages/72/14/4b0acbe45a3f01a342aae9eb346808e143caa5f1f927d3275b82bbe50129/cloudcheck-9.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:158e34819223485ed365a2f4f98b17029591a895869739afd9c5d64bfea68a09", size = 4231212, upload-time = "2026-02-03T17:37:10.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/39/6e6a144c268647ea4c8e22d1d49b8c71cb411c003976b50e703827f4305c/cloudcheck-9.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:b33bf641c96b03513c508dac40e0042dd260ae9c4ae4bcdfcbef92a91d5e4dc3", size = 1400714, upload-time = "2026-02-03T17:37:26.425Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a5/e8d933f3ffecc3d28b46a278fc58fabfe14743dd7275f68a44a7f5cdac75/cloudcheck-9.3.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4ce814065be3f6341b63d0a34e1a8fbfcd294f911d2eef87c421f0ddb21f7c93", size = 1623140, upload-time = "2026-02-03T17:36:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/b99ab305439783c832affafab081078dc9aa4b16cface7864dc33af19b14/cloudcheck-9.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60be463311be5d4525acce03aff8795c8eebb30bea4da1a5451a468812a134c7", size = 1588115, upload-time = "2026-02-03T17:36:09.451Z" }, + { url = "https://files.pythonhosted.org/packages/56/c3/46dbb012a9b80b8efd90b1abb5b1e35606a7c8f9f93b73867a12114e5836/cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cb382f9da19fe24b300cdbb10aa44d14577d7cd5b20ff6ebc0fe0bad3b8e29", size = 4165981, upload-time = "2026-02-03T17:34:59.289Z" }, + { url = "https://files.pythonhosted.org/packages/9d/58/55df60d58c6475291a9cb83185817681ac9dcd493b328f36c4cadda32598/cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da69db51d1c3b4a87a519d301f742ac52f355071a2f1acbbc65e4fc3ac7f314d", size = 3521112, upload-time = "2026-02-03T17:35:17.1Z" }, + { url = "https://files.pythonhosted.org/packages/e0/85/ab20f9f1e7619fad3e63811d7a31565fda55aeac6b53f0ae5f1d78064295/cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68ae114b27342d0fe265aee543c154a1458be6dfea4aa9f49038870c6ede45ad", size = 4113701, upload-time = "2026-02-03T17:35:48.302Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/e92f154b707ba0afe39cd5aec8689e522dd83c98b19427f44eebb8c944f9/cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5c71932bb407e1c39f275ef0d9cc0cf20f88fd1fac259b35641db91e9618b36", size = 4032043, upload-time = "2026-02-03T17:35:32.223Z" }, + { url = "https://files.pythonhosted.org/packages/e7/62/24fade88e4956aafbc839d93c1e02563dff1884ddde01e961268b78604e4/cloudcheck-9.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c90d96a4d414e2f418ed6fbd39a93550de8e51c55788673a46410f020916616e", size = 3962416, upload-time = "2026-02-03T17:35:59.646Z" }, + { url = "https://files.pythonhosted.org/packages/35/e3/9bf104f8bc635746f469753b59a42379c889183fc88c0d3727d2d50f6311/cloudcheck-9.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9aedfac28ff9994c1dde5d89bba7d9a263c7d1e3a934ed62a8ae3ed48e851fb6", size = 4563252, upload-time = "2026-02-03T17:36:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/c1/61/96261f77395e4270a218b3cfa890773d3aaab1b02d7a60af095960ee4e1c/cloudcheck-9.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:36d9afdd811998cbaebd3638e142453b2d82d5b6aeb0bfb6a41582cb9962ea4a", size = 3849843, upload-time = "2026-02-03T17:36:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/84/bc/f7111ff5eae5f8ea24b6490304c8aaed8e4b8887eb4af260feafbd77d50c/cloudcheck-9.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ac1ff7eefaf892a67f8fad7a651a07ad530faddd9c5848261dc53a3a331045c6", size = 4204717, upload-time = "2026-02-03T17:36:55.335Z" }, + { url = "https://files.pythonhosted.org/packages/49/60/3766a6d7aadd96eccc023bcd9c38b3097e0247c615efa81d5a9b1f95505e/cloudcheck-9.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee329c0996ebf0e245581a0707e5ee828fed5b761bdcd69577bc4ab4808a29d7", size = 4229135, upload-time = "2026-02-03T17:37:11.85Z" }, + { url = "https://files.pythonhosted.org/packages/2d/65/9c9bddf4a38035a93dcd168ae119477a3761e2a2e5d53d3b53d3ae385dfd/cloudcheck-9.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cfc70425ba37fae7a44a66a3833ef994b99f039c5a621f523852f61b6eb320c7", size = 1397022, upload-time = "2026-02-03T17:37:27.875Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/eed1ec8dac30d4217a563770a7058a3cd8168e68940f70ec4923a8c5dcd8/cloudcheck-9.3.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ed2e9171a41786f2454902b209fe999146dc2991c1d7d0ed68fe86bbb177552a", size = 1622660, upload-time = "2026-02-03T17:36:18.042Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/3bd76bb2ae378256126d17a73d12512bd0753a8de1397a394423ef610b91/cloudcheck-9.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1651903604090d5f4dc671c243383e87cd0ab53d7a34d4e7887d82e9f2077a28", size = 1587067, upload-time = "2026-02-03T17:36:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/89/23/c1b9174670c083e36acfe3a74a681fd98bfaea17334a2c23e1e9bcbea5ca/cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05ec385d95adef0a420a51a1df97d17b6c29d3030b2f2b1ffca5de1ea85ee7a5", size = 4165467, upload-time = "2026-02-03T17:35:00.797Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7c/033d73019a13f11b18614f64e75e899cdcc6f563247731d0c62acd1dd19c/cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c477506721b87d7e0a6a13386bd57feb5ab1615cbcdd9d62971640df24ba70cc", size = 3520715, upload-time = "2026-02-03T17:35:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/34/e4/65fd6998cdedf803330629b37ecc0d23fc0cccba17f271b0bddae89e518b/cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a996011efef6af71f2f712fbe9bc9fefd49216c0dffc648528abd329f6003a0", size = 4112571, upload-time = "2026-02-03T17:35:49.782Z" }, + { url = "https://files.pythonhosted.org/packages/82/e1/abfe64139dcb6af7a0cbd8ca12216148e77245afea10eba1e1c7725c11a3/cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af152cf8e1b2a845db3412b131d6c8c6964cff161aad9500b56bd383ec028936", size = 4029919, upload-time = "2026-02-03T17:35:33.953Z" }, + { url = "https://files.pythonhosted.org/packages/49/1b/416f35057e2ff464810e760cef5fc735dab1d6c1dfd0066b8cb34e4ea1da/cloudcheck-9.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:359e7c66015d3245d636ce03aa527bf6d69c2e0f72278857a2b51e9673df9904", size = 3961613, upload-time = "2026-02-03T17:36:01.172Z" }, + { url = "https://files.pythonhosted.org/packages/97/d0/fb6c7af398f428423c7024e1ce8f08624ee38a4cbd768af0c2682792e31e/cloudcheck-9.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a8138b78e7a49814ef6bf56f0de9b35c1e53473fd83cabb451db3e740ab5e83", size = 4562157, upload-time = "2026-02-03T17:36:27.559Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4f/91f460dbf13acbe052ea128aeef27d97de5d8a098247493a83760ea37de8/cloudcheck-9.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:22f3645c1eb67a3489c7ebbfef4eb3c1f39187ab54a5d61703cb26df8b477d38", size = 3848678, upload-time = "2026-02-03T17:36:41.997Z" }, + { url = "https://files.pythonhosted.org/packages/72/cc/880c660f04ad1eea12866ce4b513ac29c51e2d86d8518fbf1bb7934b75b7/cloudcheck-9.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:78b2f7d8235f9d5fe2d3670c125769c65b94cca1e0170d682069bb478b20ffc8", size = 4203721, upload-time = "2026-02-03T17:36:56.95Z" }, + { url = "https://files.pythonhosted.org/packages/2c/05/cdf0c5a3d86e25415e54e2fbdc81d8e36384c5d998cb3f96ec9202fb05a7/cloudcheck-9.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:360b80aad144c2fbf8cf251587af714f51d58b02e76593d60da40b20a6ba6140", size = 4227912, upload-time = "2026-02-03T17:37:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/2c/cf/c4aa573d6bc0d6d9ddf60d8dd6df1e3d15b966f92ccb09ebd207d25b8e98/cloudcheck-9.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:d623b523de9d24297fc6d337302e12faf8ead6c5ab17bcbf39cbed1ec7f7abe1", size = 1396770, upload-time = "2026-02-03T17:37:29.563Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5f/bf37567f1597deb72cf0a3cd57d27817b7d868164215516eb96e2dee112c/cloudcheck-9.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2033d75451653babb908394f00a78ead9cb66481f7ca88f957b74fdff050a0b9", size = 4164144, upload-time = "2026-02-03T17:35:02.84Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/e45a21c4e9b54b885170f495016f105b68dda8e8f8b965cbacde37791dcf/cloudcheck-9.3.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b504627920b80cc4695b881a1e58be109abdc482be8202865d11c028865ff7e3", size = 3518061, upload-time = "2026-02-03T17:35:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/40/e72ecf531a3e7848de7df9704bea5de200c3c6309e64108139d00b0c1bd4/cloudcheck-9.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0edb7e05e289852ca026cfa97fea8c86d369a3a6a061edeaf47939a31c745cc2", size = 4031957, upload-time = "2026-02-03T17:35:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/96/50/4f9e8a1ea2f6e465e42d75b76e07d3da336ff603acf4c02d4d847c92d661/cloudcheck-9.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:99509b9cefc95cff71bb0cda4651ec3b931202512c41583940e471038cb0f288", size = 4559939, upload-time = "2026-02-03T17:36:29.075Z" }, + { url = "https://files.pythonhosted.org/packages/0e/37/9eb5d2237ea85a447698368f07f3f3f0e1b8d5b1b72385b2439527efb792/cloudcheck-9.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:138e6578db91123a2aafc21a7ee89d302ceec49891b1257364832cd9a4f5ad62", size = 3845175, upload-time = "2026-02-03T17:36:43.572Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5a/73c6b39ee3a9cbdb9c4d9fca543d988a60cdaf029ae049fe1ed0b533bda5/cloudcheck-9.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4058bbda0b578853288af0bb58de52257cfcafd40b8609a199d5d2b71ec773d9", size = 4199475, upload-time = "2026-02-03T17:36:58.999Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/025a6b01b25e6fd9c1501772fb386f42c79927cdcc4d4a2e9030b58bb7b3/cloudcheck-9.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8eb3e1af683f327f0eb7dbe1fc93fb07d271b69e0045540d566830fae7855dab", size = 4230077, upload-time = "2026-02-03T17:37:16.286Z" }, + { url = "https://files.pythonhosted.org/packages/95/94/aed52ba78556cf9d049dfcd265d1d6214a6a78ccff81dd68c1729801ee71/cloudcheck-9.3.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b4415fd866000257dae58d9b5ab58fb2c95225b65e770f3badee33d3ae4c2989", size = 1623052, upload-time = "2026-02-03T17:36:20.563Z" }, + { url = "https://files.pythonhosted.org/packages/aa/57/fded827f83f8fa5ae9e38f038c825955025898a9788dbee5280f5dc30a71/cloudcheck-9.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:530874ef87665d6e14b4756b85b95a4c27916804a6778125851b49203ae037c4", size = 1587276, upload-time = "2026-02-03T17:36:13.502Z" }, + { url = "https://files.pythonhosted.org/packages/84/dd/233f12e63440374c5949b39dcde2382346a79f0a117660c348c34ba7a170/cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d37ed257e26a21389b99b1c7ad414c3d24b56eab21686a549f8ebf2bdc1dd48", size = 4167268, upload-time = "2026-02-03T17:35:04.723Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a7/cf8aac0d334f2ebdad8562dbd7e46f5e8acadceabf9d8ce3f7cd918b16b7/cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3fcb7b0332656c9166bc09977559bad260df9dcb6bcac3baa980842c2017a4", size = 3519999, upload-time = "2026-02-03T17:35:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/63/89/9be9aa3fbdb4a130159ea7c74a4e4123e12be2e20f911bb6e8649a42b77d/cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89347b3e458262119a7f94d5ff2e0d535996a6dd7b501a222a28b8b235379e40", size = 4110767, upload-time = "2026-02-03T17:35:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/f3/80/aec26543ab4efd3e9b1c69746ba48464ccc726e0b22eb174ebfd9096cdeb/cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:252fd606307b4a039b34ff928d482302b323217d92b94eadc9019c81f1231e61", size = 4030036, upload-time = "2026-02-03T17:35:37.058Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9a/c06aed3e79f62b4184be89fa6f32edbb1f20ce86ee49fb1a9245e7899b4d/cloudcheck-9.3.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86a9b96fcd645e028980db0e25482b1af13300c5e4e76fcd6707adffd9552220", size = 3960739, upload-time = "2026-02-03T17:36:02.644Z" }, + { url = "https://files.pythonhosted.org/packages/14/94/fb37c742e32009abfae262e32cc4dc32760fd8a3c05e73ebbad3265f4948/cloudcheck-9.3.0-cp314-cp314-manylinux_2_38_x86_64.whl", hash = "sha256:c055966a04d21b4728e525633d7f0ff5713b76bac9024679ab20ff2e8050e5ba", size = 3553719, upload-time = "2026-02-03T17:19:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/4e03e1fa4f3ebdeb9b56271bd9130af3a6631ed36a6acb24ab935782a610/cloudcheck-9.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3171964cb5e204d17192cf12b79210b0f36457786af187c89407eae297f015fe", size = 4562957, upload-time = "2026-02-03T17:36:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/12/39/1dbea307334ada4a640b1a7dcf8b5607d231d1beae35aba6682d2c993f67/cloudcheck-9.3.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4fdb2cb2727f40e5e4d66a3c43895f0892c72f9615142a190271d9b91dc634c5", size = 3849514, upload-time = "2026-02-03T17:36:45.524Z" }, + { url = "https://files.pythonhosted.org/packages/58/ff/f5d829e36a1a6f85f18a147ff20c544478359397652f622e32b37d044eb3/cloudcheck-9.3.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fa2352c765342aefa2c0e6a75093efb75fafaab51f86e36c4b32849e0ef18ee8", size = 4202797, upload-time = "2026-02-03T17:37:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/05/0d/6b2847f41e791157829ad71d2aa7e259c38a5f49c211fde60663770fdde5/cloudcheck-9.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:146c545702267071e1a375d8ca8bbd7a4fa5e0f87ac6adfd13fc8835bb3d2bc7", size = 4227095, upload-time = "2026-02-03T17:37:18.3Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6f/14f52d56f6dfdbf49366d0500d41e83887b440d00096669adf06d5788411/cloudcheck-9.3.0-cp314-cp314-win32.whl", hash = "sha256:a95b840efe2616231c99a9ef5be4e8484b880af5e3e9eeab81bf473cbee70088", size = 1297125, upload-time = "2026-02-03T17:37:32.623Z" }, + { url = "https://files.pythonhosted.org/packages/63/c1/a60dc7d844859ff663b6f5dd72890675ac8d3a3d4552990b202b653e565c/cloudcheck-9.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:9d631e21b3945615739f7862e1e378b2f3f43d4409a62bc657e858762f83ac67", size = 1397209, upload-time = "2026-02-03T17:37:31.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/2b/2d313a4c8ac4b3212c145daf1cf2c407887d5585ebe17ca15fb7ff72be0e/cloudcheck-9.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:681ef11beeebfbf6205b0e05a6d151943a533e6e6124f0399b9128692b350c63", size = 4163997, upload-time = "2026-02-03T17:35:06.257Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/0ee67e21a98327b2ce2ba5a8eea6ff4317d28cb7bd27afcab61ba98e49c5/cloudcheck-9.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f96f1ebc2b30c7c790b62f1a7c13909230502c457b445cd96d1803c4434da6bb", size = 3517979, upload-time = "2026-02-03T17:35:23.419Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6d/cc3da21a8a7f63786e3cf5ad3007db73f49f051e6471e967c528424a6bc6/cloudcheck-9.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5895e5dd24a3e9f1d3412e15ff96fdd0a6f58d0a1ea192deba6f02926331054", size = 4030537, upload-time = "2026-02-03T17:35:38.541Z" }, + { url = "https://files.pythonhosted.org/packages/d7/96/62f3012f9181ef17400490d527db0e6180cf0f25de3eb7f9f854800ba869/cloudcheck-9.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8407b08b382a6bcb23ab77ce3a12dfdf075feff418c911f7a385701a20b8df34", size = 4560503, upload-time = "2026-02-03T17:36:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/d3126053275e4abc21f49643914c26b344df91c44da77790f481cb266cff/cloudcheck-9.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4dab2a77055204e014c31cf7466f23687612de66579b81e3c18c00d3eeaa526b", size = 3844998, upload-time = "2026-02-03T17:36:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/19/9e/45aa754f49b82365e524aceb67484880fee53d9e728d6a369e0a62343cd9/cloudcheck-9.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:68088d71a16ac55390ec53240642b3cf26f98692035e1ed425a3c35144ca1f26", size = 4201211, upload-time = "2026-02-03T17:37:03.595Z" }, + { url = "https://files.pythonhosted.org/packages/f3/20/c7617ad4da53f90d36c40275314141cfeb74ace10c5398d2742cf772c72f/cloudcheck-9.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5615d361881b15f36afd88136bc5af221ff1794b18c49616411c32578a69de28", size = 4228974, upload-time = "2026-02-03T17:37:19.92Z" }, + { url = "https://files.pythonhosted.org/packages/ae/31/fbf97823e0730c579c3ecde4ae6a94132312071724786b9f873d77cba0e1/cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee5e5b240d39c14829576b841411d6a4dd867c1c1d4f23e5aadf910151b25ed1", size = 4171611, upload-time = "2026-02-03T17:35:10.973Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1d/da2809e0065d0ea93091200285f7313cce04517b4284791a593a770c7804/cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d5fe3f4e1fef0a83ffd2bfa2d4aa8b4c83aea2c7116fb83e75dcf82780aeb5dd", size = 3523200, upload-time = "2026-02-03T17:35:26.796Z" }, + { url = "https://files.pythonhosted.org/packages/ae/98/bb46d6cd5c97bc0201a64337edb9aed7bc6a11c22b6b4da982023013f3e4/cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39ec7ebae9a8a1ec42d5ec047d0506584576aa1cb32d7d4c3fff5db241844ebe", size = 4118696, upload-time = "2026-02-03T17:35:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/87/2c/29dc618cbf5a3699166a86934cb28cb78275459ebbc602729034aab2fe76/cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92eb00480d651e8ee7d922005804dcc10a30d05e8a1feb7d49d93e11dd7b7b82", size = 4037811, upload-time = "2026-02-03T17:35:43.838Z" }, + { url = "https://files.pythonhosted.org/packages/08/b8/8919ffe57f1fb23def9bc8899e0eae3dad72d2bee87d44dce7b988949626/cloudcheck-9.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0befb18f1b563985c68ce3aae0d58363939af66e13a15f0defbab1a2bd512459", size = 3964908, upload-time = "2026-02-03T17:36:05.847Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/d8ba1101030ec2e165da7acd103e414a02bc610a9972deec02587447d7b8/cloudcheck-9.3.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:66940758bf97c40db14c1f7457ece633503a91803191c84742f3a938b5fbc9d8", size = 4565725, upload-time = "2026-02-03T17:36:35.795Z" }, + { url = "https://files.pythonhosted.org/packages/31/a0/83fbc2f615a7f995a3f4dadc5909998b3ab3967678d53e5ea9b1d6629588/cloudcheck-9.3.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:5d97d3ecd2917b75b518766281be408253f6f59a2d09db54f7ecf9d847c6de3a", size = 3852289, upload-time = "2026-02-03T17:36:50.138Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/cbc543a16eb8e036f0bc9be7687d3e3c4fc46883c57609896b8084219976/cloudcheck-9.3.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:f593b1a400f8b0ec3995b56126efb001729b46bac4c76d6d2399e8ab62e49515", size = 4207939, upload-time = "2026-02-03T17:37:07.172Z" }, + { url = "https://files.pythonhosted.org/packages/40/80/d4ba464388e7ae68b36f81d3091cbe9be6b0dff450f24612552205e93d91/cloudcheck-9.3.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4854fc0aa88ec38275f0c2f8057803c1c37eec93d9f4c5e9f0e0a5b38fd6604f", size = 4230542, upload-time = "2026-02-03T17:37:22.976Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "deepdiff" +version = "8.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "orderly-set" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/76/36c9aab3d5c19a94091f7c6c6e784efca50d87b124bf026c36e94719f33c/deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a", size = 634054, upload-time = "2025-09-03T19:40:41.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b", size = 91378, upload-time = "2025-09-03T19:40:39.679Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, +] + +[[package]] +name = "filelock" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/6b/cc63cdbff46eba1ce2fbd058e9699f99c43f7e604da15413ca0331040bff/filelock-3.21.0.tar.gz", hash = "sha256:48c739c73c6fcacd381ed532226991150947c4a76dcd674f84d6807fd55dbaf2", size = 31341, upload-time = "2026-02-12T15:40:48.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/ab/05190b5a64101fcb743bc63a034c0fac86a515c27c303c69221093565f28/filelock-3.21.0-py3-none-any.whl", hash = "sha256:0f90eee4c62101243df3007db3cf8fc3ebf1bb13541d3e72c687d6e0f3f7d531", size = 21381, upload-time = "2026-02-12T15:40:46.964Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "libsass" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b4/ab091585eaa77299558e3289ca206846aefc123fb320b5656ab2542c20ad/libsass-0.23.0.tar.gz", hash = "sha256:6f209955ede26684e76912caf329f4ccb57e4a043fd77fe0e7348dd9574f1880", size = 316068, upload-time = "2024-01-06T18:53:05.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/13/fc1bea1de880ca935137183727c7d4dd921c4128fc08b8ddc3698ba5a8a3/libsass-0.23.0-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:34cae047cbbfc4ffa832a61cbb110f3c95f5471c6170c842d3fed161e40814dc", size = 1086783, upload-time = "2024-01-06T19:02:38.903Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/6af938651ff3aec0a0b00742209df1172bc297fa73531f292801693b7315/libsass-0.23.0-cp38-abi3-macosx_14_0_arm64.whl", hash = "sha256:ea97d1b45cdc2fc3590cb9d7b60f1d8915d3ce17a98c1f2d4dd47ee0d9c68ce6", size = 982759, upload-time = "2024-01-06T19:02:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/fd/5a/eb5b62641df0459a3291fc206cf5bd669c0feed7814dded8edef4ade8512/libsass-0.23.0-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306", size = 9444543, upload-time = "2024-01-06T19:02:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fc/275783f5120970d859ae37d04b6a60c13bdec2aa4294b9dfa8a37b5c2513/libsass-0.23.0-cp38-abi3-win32.whl", hash = "sha256:31e86d92a5c7a551df844b72d83fc2b5e50abc6fbbb31e296f7bebd6489ed1b4", size = 775481, upload-time = "2024-01-06T19:02:46.05Z" }, + { url = "https://files.pythonhosted.org/packages/ef/20/caf3c7cf2432d85263119798c45221ddf67bdd7dae8f626d14ff8db04040/libsass-0.23.0-cp38-abi3-win_amd64.whl", hash = "sha256:a2ec85d819f353cbe807432d7275d653710d12b08ec7ef61c124a580a8352f3c", size = 872914, upload-time = "2024-01-06T19:02:47.61Z" }, +] + +[[package]] +name = "livereload" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/6e/f2748665839812a9bbe5c75d3f983edbf3ab05fa5cd2f7c2f36fffdf65bd/livereload-2.7.1.tar.gz", hash = "sha256:3d9bf7c05673df06e32bea23b494b8d36ca6d10f7d5c3c8a6989608c09c986a9", size = 22255, upload-time = "2024-12-18T13:42:01.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/3e/de54dc7f199e85e6ca37e2e5dae2ec3bce2151e9e28f8eb9076d71e83d56/livereload-2.7.1-py3-none-any.whl", hash = "sha256:5201740078c1b9433f4b2ba22cd2729a39b9d0ec0a2cc6b4d3df257df5ad0564", size = 22657, upload-time = "2024-12-18T13:41:56.35Z" }, +] + +[[package]] +name = "lockfile" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/47/72cb04a58a35ec495f96984dddb48232b551aafb95bde614605b754fe6f7/lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799", size = 20874, upload-time = "2015-11-25T18:29:58.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa", size = 13564, upload-time = "2015-11-25T18:29:51.462Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mike" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "importlib-resources" }, + { name = "jinja2" }, + { name = "mkdocs" }, + { name = "pyparsing" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "verspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/f7/2933f1a1fb0e0f077d5d6a92c6c7f8a54e6128241f116dff4df8b6050bbf/mike-2.1.3.tar.gz", hash = "sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810", size = 38119, upload-time = "2024-08-13T05:02:14.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl", hash = "sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a", size = 33754, upload-time = "2024-08-13T05:02:12.515Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-extra-sass-plugin" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "libsass" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/09/96284d51afae05aa35ffb17617a4ef819c06852bea566b935a1c1119660c/mkdocs-extra-sass-plugin-0.1.0.tar.gz", hash = "sha256:cca7ae778585514371b22a63bcd69373d77e474edab4b270cf2924e05c879219", size = 4973, upload-time = "2021-01-30T04:20:44.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/25/620659b5f9390b9b3ce2c0017cd3aef76d112a87bc2800640bdc2727b597/mkdocs_extra_sass_plugin-0.1.0-py3-none-any.whl", hash = "sha256:10aa086fa8ef1fc4650f7bb6927deb7bf5bbf5a2dd3178f47e4ef44546b156db", size = 5352, upload-time = "2021-01-30T04:20:46.064Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/84/78243847ad9d5c21d30a2842720425b17e880d99dfe824dee11d6b2149b4/mkdocstrings_python-2.0.2.tar.gz", hash = "sha256:4a32ccfc4b8d29639864698e81cfeb04137bce76bb9f3c251040f55d4b6e1ad8", size = 199124, upload-time = "2026-02-09T15:12:01.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/31/7ee938abbde2322e553a2cb5f604cdd1e4728e08bba39c7ee6fae9af840b/mkdocstrings_python-2.0.2-py3-none-any.whl", hash = "sha256:31241c0f43d85a69306d704d5725786015510ea3f3c4bdfdb5a5731d83cdc2b0", size = 104900, upload-time = "2026-02-09T15:12:00.166Z" }, +] + +[[package]] +name = "mmh3" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582, upload-time = "2025-07-29T07:43:48.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/2b/870f0ff5ecf312c58500f45950751f214b7068665e66e9bfd8bc2595587c/mmh3-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc", size = 56119, upload-time = "2025-07-29T07:41:39.117Z" }, + { url = "https://files.pythonhosted.org/packages/3b/88/eb9a55b3f3cf43a74d6bfa8db0e2e209f966007777a1dc897c52c008314c/mmh3-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328", size = 40634, upload-time = "2025-07-29T07:41:40.626Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4c/8e4b3878bf8435c697d7ce99940a3784eb864521768069feaccaff884a17/mmh3-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d", size = 40080, upload-time = "2025-07-29T07:41:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/0a254402c8c5ca424a0a9ebfe870f5665922f932830f0a11a517b6390a09/mmh3-5.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e", size = 95321, upload-time = "2025-07-29T07:41:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/29306d5eca6dfda4b899d22c95b5420db4e0ffb7e0b6389b17379654ece5/mmh3-5.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515", size = 101220, upload-time = "2025-07-29T07:41:43.572Z" }, + { url = "https://files.pythonhosted.org/packages/49/f7/0dd1368e531e52a17b5b8dd2f379cce813bff2d0978a7748a506f1231152/mmh3-5.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3", size = 103991, upload-time = "2025-07-29T07:41:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/35/06/abc7122c40f4abbfcef01d2dac6ec0b77ede9757e5be8b8a40a6265b1274/mmh3-5.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e", size = 110894, upload-time = "2025-07-29T07:41:45.849Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/837885759afa4baccb8e40456e1cf76a4f3eac835b878c727ae1286c5f82/mmh3-5.2.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d", size = 118327, upload-time = "2025-07-29T07:41:47.224Z" }, + { url = "https://files.pythonhosted.org/packages/40/cc/5683ba20a21bcfb3f1605b1c474f46d30354f728a7412201f59f453d405a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772", size = 101701, upload-time = "2025-07-29T07:41:48.259Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/99ab3fb940150aec8a26dbdfc39b200b5592f6aeb293ec268df93e054c30/mmh3-5.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89", size = 96712, upload-time = "2025-07-29T07:41:49.467Z" }, + { url = "https://files.pythonhosted.org/packages/61/04/d7c4cb18f1f001ede2e8aed0f9dbbfad03d161c9eea4fffb03f14f4523e5/mmh3-5.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2", size = 110302, upload-time = "2025-07-29T07:41:50.387Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bf/4dac37580cfda74425a4547500c36fa13ef581c8a756727c37af45e11e9a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc", size = 111929, upload-time = "2025-07-29T07:41:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b1/49f0a582c7a942fb71ddd1ec52b7d21d2544b37d2b2d994551346a15b4f6/mmh3-5.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106", size = 100111, upload-time = "2025-07-29T07:41:53.139Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/ccec09f438caeb2506f4c63bb3b99aa08a9e09880f8fc047295154756210/mmh3-5.2.0-cp310-cp310-win32.whl", hash = "sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d", size = 40783, upload-time = "2025-07-29T07:41:54.463Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f4/8d39a32c8203c1cdae88fdb04d1ea4aa178c20f159df97f4c5a2eaec702c/mmh3-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb", size = 41549, upload-time = "2025-07-29T07:41:55.295Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/30efb1cd945e193f62574144dd92a0c9ee6463435e4e8ffce9b9e9f032f0/mmh3-5.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8", size = 39335, upload-time = "2025-07-29T07:41:56.194Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/399567b3796e134352e11a8b973cd470c06b2ecfad5468fe580833be442b/mmh3-5.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1", size = 56107, upload-time = "2025-07-29T07:41:57.07Z" }, + { url = "https://files.pythonhosted.org/packages/c3/09/830af30adf8678955b247d97d3d9543dd2fd95684f3cd41c0cd9d291da9f/mmh3-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051", size = 40635, upload-time = "2025-07-29T07:41:57.903Z" }, + { url = "https://files.pythonhosted.org/packages/07/14/eaba79eef55b40d653321765ac5e8f6c9ac38780b8a7c2a2f8df8ee0fb72/mmh3-5.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10", size = 40078, upload-time = "2025-07-29T07:41:58.772Z" }, + { url = "https://files.pythonhosted.org/packages/bb/26/83a0f852e763f81b2265d446b13ed6d49ee49e1fc0c47b9655977e6f3d81/mmh3-5.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c", size = 97262, upload-time = "2025-07-29T07:41:59.678Z" }, + { url = "https://files.pythonhosted.org/packages/00/7d/b7133b10d12239aeaebf6878d7eaf0bf7d3738c44b4aba3c564588f6d802/mmh3-5.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762", size = 103118, upload-time = "2025-07-29T07:42:01.197Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3e/62f0b5dce2e22fd5b7d092aba285abd7959ea2b17148641e029f2eab1ffa/mmh3-5.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4", size = 106072, upload-time = "2025-07-29T07:42:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/66/84/ea88bb816edfe65052c757a1c3408d65c4201ddbd769d4a287b0f1a628b2/mmh3-5.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363", size = 112925, upload-time = "2025-07-29T07:42:03.632Z" }, + { url = "https://files.pythonhosted.org/packages/2e/13/c9b1c022807db575fe4db806f442d5b5784547e2e82cff36133e58ea31c7/mmh3-5.2.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8", size = 120583, upload-time = "2025-07-29T07:42:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/0e2dfe1a38f6a78788b7eb2b23432cee24623aeabbc907fed07fc17d6935/mmh3-5.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed", size = 99127, upload-time = "2025-07-29T07:42:05.929Z" }, + { url = "https://files.pythonhosted.org/packages/77/27/aefb7d663b67e6a0c4d61a513c83e39ba2237e8e4557fa7122a742a23de5/mmh3-5.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646", size = 98544, upload-time = "2025-07-29T07:42:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/ab/97/a21cc9b1a7c6e92205a1b5fa030cdf62277d177570c06a239eca7bd6dd32/mmh3-5.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b", size = 106262, upload-time = "2025-07-29T07:42:07.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/18/db19ae82ea63c8922a880e1498a75342311f8aa0c581c4dd07711473b5f7/mmh3-5.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779", size = 109824, upload-time = "2025-07-29T07:42:08.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f5/41dcf0d1969125fc6f61d8618b107c79130b5af50b18a4651210ea52ab40/mmh3-5.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2", size = 97255, upload-time = "2025-07-29T07:42:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/cce9eaa0efac1f0e735bb178ef9d1d2887b4927fe0ec16609d5acd492dda/mmh3-5.2.0-cp311-cp311-win32.whl", hash = "sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28", size = 40779, upload-time = "2025-07-29T07:42:10.546Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/3fa0290122e6d5a7041b50ae500b8a9f4932478a51e48f209a3879fe0b9b/mmh3-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee", size = 41549, upload-time = "2025-07-29T07:42:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/3a/54/c277475b4102588e6f06b2e9095ee758dfe31a149312cdbf62d39a9f5c30/mmh3-5.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9", size = 39336, upload-time = "2025-07-29T07:42:12.209Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6a/d5aa7edb5c08e0bd24286c7d08341a0446f9a2fbbb97d96a8a6dd81935ee/mmh3-5.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be", size = 56141, upload-time = "2025-07-29T07:42:13.456Z" }, + { url = "https://files.pythonhosted.org/packages/08/49/131d0fae6447bc4a7299ebdb1a6fb9d08c9f8dcf97d75ea93e8152ddf7ab/mmh3-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd", size = 40681, upload-time = "2025-07-29T07:42:14.306Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6f/9221445a6bcc962b7f5ff3ba18ad55bba624bacdc7aa3fc0a518db7da8ec/mmh3-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96", size = 40062, upload-time = "2025-07-29T07:42:15.08Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d4/6bb2d0fef81401e0bb4c297d1eb568b767de4ce6fc00890bc14d7b51ecc4/mmh3-5.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094", size = 97333, upload-time = "2025-07-29T07:42:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/44/e0/ccf0daff8134efbb4fbc10a945ab53302e358c4b016ada9bf97a6bdd50c1/mmh3-5.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037", size = 103310, upload-time = "2025-07-29T07:42:17.796Z" }, + { url = "https://files.pythonhosted.org/packages/02/63/1965cb08a46533faca0e420e06aff8bbaf9690a6f0ac6ae6e5b2e4544687/mmh3-5.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773", size = 106178, upload-time = "2025-07-29T07:42:19.281Z" }, + { url = "https://files.pythonhosted.org/packages/c2/41/c883ad8e2c234013f27f92061200afc11554ea55edd1bcf5e1accd803a85/mmh3-5.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5", size = 113035, upload-time = "2025-07-29T07:42:20.356Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/1ccade8b1fa625d634a18bab7bf08a87457e09d5ec8cf83ca07cbea9d400/mmh3-5.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50", size = 120784, upload-time = "2025-07-29T07:42:21.377Z" }, + { url = "https://files.pythonhosted.org/packages/77/1c/919d9171fcbdcdab242e06394464ccf546f7d0f3b31e0d1e3a630398782e/mmh3-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765", size = 99137, upload-time = "2025-07-29T07:42:22.344Z" }, + { url = "https://files.pythonhosted.org/packages/66/8a/1eebef5bd6633d36281d9fc83cf2e9ba1ba0e1a77dff92aacab83001cee4/mmh3-5.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43", size = 98664, upload-time = "2025-07-29T07:42:23.269Z" }, + { url = "https://files.pythonhosted.org/packages/13/41/a5d981563e2ee682b21fb65e29cc0f517a6734a02b581359edd67f9d0360/mmh3-5.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4", size = 106459, upload-time = "2025-07-29T07:42:24.238Z" }, + { url = "https://files.pythonhosted.org/packages/24/31/342494cd6ab792d81e083680875a2c50fa0c5df475ebf0b67784f13e4647/mmh3-5.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3", size = 110038, upload-time = "2025-07-29T07:42:25.629Z" }, + { url = "https://files.pythonhosted.org/packages/28/44/efda282170a46bb4f19c3e2b90536513b1d821c414c28469a227ca5a1789/mmh3-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c", size = 97545, upload-time = "2025-07-29T07:42:27.04Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/534ae319c6e05d714f437e7206f78c17e66daca88164dff70286b0e8ea0c/mmh3-5.2.0-cp312-cp312-win32.whl", hash = "sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49", size = 40805, upload-time = "2025-07-29T07:42:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f6/f6abdcfefcedab3c964868048cfe472764ed358c2bf6819a70dd4ed4ed3a/mmh3-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3", size = 41597, upload-time = "2025-07-29T07:42:28.894Z" }, + { url = "https://files.pythonhosted.org/packages/15/fd/f7420e8cbce45c259c770cac5718badf907b302d3a99ec587ba5ce030237/mmh3-5.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0", size = 39350, upload-time = "2025-07-29T07:42:29.794Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874, upload-time = "2025-07-29T07:42:30.662Z" }, + { url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012, upload-time = "2025-07-29T07:42:31.539Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197, upload-time = "2025-07-29T07:42:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840, upload-time = "2025-07-29T07:42:33.227Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644, upload-time = "2025-07-29T07:42:34.099Z" }, + { url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153, upload-time = "2025-07-29T07:42:35.031Z" }, + { url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684, upload-time = "2025-07-29T07:42:35.914Z" }, + { url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057, upload-time = "2025-07-29T07:42:36.755Z" }, + { url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344, upload-time = "2025-07-29T07:42:37.675Z" }, + { url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325, upload-time = "2025-07-29T07:42:38.591Z" }, + { url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240, upload-time = "2025-07-29T07:42:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060, upload-time = "2025-07-29T07:42:40.585Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781, upload-time = "2025-07-29T07:42:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174, upload-time = "2025-07-29T07:42:42.587Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734, upload-time = "2025-07-29T07:42:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493, upload-time = "2025-07-29T07:42:45.07Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089, upload-time = "2025-07-29T07:42:46.122Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571, upload-time = "2025-07-29T07:42:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806, upload-time = "2025-07-29T07:42:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600, upload-time = "2025-07-29T07:42:49.371Z" }, + { url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349, upload-time = "2025-07-29T07:42:50.268Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/a20db059a8a47048aaf550da14a145b56e9c7386fb8280d3ce2962dcebf7/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078", size = 39209, upload-time = "2025-07-29T07:42:51.559Z" }, + { url = "https://files.pythonhosted.org/packages/98/dd/e5094799d55c7482d814b979a0fd608027d0af1b274bfb4c3ea3e950bfd5/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501", size = 39843, upload-time = "2025-07-29T07:42:52.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/7844d7f832c85400e7cc89a1348e4e1fdd38c5a38415bb5726bbb8fcdb6c/mmh3-5.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b", size = 40648, upload-time = "2025-07-29T07:42:53.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bf/71f791f48a21ff3190ba5225807cbe4f7223360e96862c376e6e3fb7efa7/mmh3-5.2.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770", size = 56164, upload-time = "2025-07-29T07:42:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/70/1f/f87e3d34d83032b4f3f0f528c6d95a98290fcacf019da61343a49dccfd51/mmh3-5.2.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110", size = 40692, upload-time = "2025-07-29T07:42:55.234Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e2/db849eaed07117086f3452feca8c839d30d38b830ac59fe1ce65af8be5ad/mmh3-5.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647", size = 40068, upload-time = "2025-07-29T07:42:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/209af927207af77425b044e32f77f49105a0b05d82ff88af6971d8da4e19/mmh3-5.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63", size = 97367, upload-time = "2025-07-29T07:42:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/78adf4104c425606a9ce33fb351f790c76a6c2314969c4a517d1ffc92196/mmh3-5.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12", size = 103306, upload-time = "2025-07-29T07:42:58.522Z" }, + { url = "https://files.pythonhosted.org/packages/a3/79/c2b89f91b962658b890104745b1b6c9ce38d50a889f000b469b91eeb1b9e/mmh3-5.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22", size = 106312, upload-time = "2025-07-29T07:42:59.552Z" }, + { url = "https://files.pythonhosted.org/packages/4b/14/659d4095528b1a209be90934778c5ffe312177d51e365ddcbca2cac2ec7c/mmh3-5.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5", size = 113135, upload-time = "2025-07-29T07:43:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6f/cd7734a779389a8a467b5c89a48ff476d6f2576e78216a37551a97e9e42a/mmh3-5.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07", size = 120775, upload-time = "2025-07-29T07:43:02.124Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ca/8256e3b96944408940de3f9291d7e38a283b5761fe9614d4808fcf27bd62/mmh3-5.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935", size = 99178, upload-time = "2025-07-29T07:43:03.182Z" }, + { url = "https://files.pythonhosted.org/packages/8a/32/39e2b3cf06b6e2eb042c984dab8680841ac2a0d3ca6e0bea30db1f27b565/mmh3-5.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7", size = 98738, upload-time = "2025-07-29T07:43:04.207Z" }, + { url = "https://files.pythonhosted.org/packages/61/d3/7bbc8e0e8cf65ebbe1b893ffa0467b7ecd1bd07c3bbf6c9db4308ada22ec/mmh3-5.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5", size = 106510, upload-time = "2025-07-29T07:43:05.656Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/b97e53724b52374e2f3859046f0eb2425192da356cb19784d64bc17bb1cf/mmh3-5.2.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384", size = 110053, upload-time = "2025-07-29T07:43:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/ac/62/3688c7d975ed195155671df68788c83fed6f7909b6ec4951724c6860cb97/mmh3-5.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e", size = 97546, upload-time = "2025-07-29T07:43:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ca/3b/c6153250f03f71a8b7634cded82939546cdfba02e32f124ff51d52c6f991/mmh3-5.2.0-cp314-cp314-win32.whl", hash = "sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0", size = 41422, upload-time = "2025-07-29T07:43:09.216Z" }, + { url = "https://files.pythonhosted.org/packages/74/01/a27d98bab083a435c4c07e9d1d720d4c8a578bf4c270bae373760b1022be/mmh3-5.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b", size = 42135, upload-time = "2025-07-29T07:43:10.183Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c9/dbba5507e95429b8b380e2ba091eff5c20a70a59560934dff0ad8392b8c8/mmh3-5.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115", size = 39879, upload-time = "2025-07-29T07:43:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/c8c0ef839c17258b9de41b84f663574fabcf8ac2007b7416575e0f65ff6e/mmh3-5.2.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932", size = 57696, upload-time = "2025-07-29T07:43:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/95e2b9ff201e89f9fe37036037ab61a6c941942b25cdb7b6a9df9b931993/mmh3-5.2.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c", size = 41421, upload-time = "2025-07-29T07:43:13.269Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/9be23ad0b7001a4b22752e7693be232428ecc0a35068a4ff5c2f14ef8b20/mmh3-5.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be", size = 40853, upload-time = "2025-07-29T07:43:14.888Z" }, + { url = "https://files.pythonhosted.org/packages/ac/1b/96b32058eda1c1dee8264900c37c359a7325c1f11f5ff14fd2be8e24eff9/mmh3-5.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb", size = 109694, upload-time = "2025-07-29T07:43:15.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6f/a2ae44cd7dad697b6dea48390cbc977b1e5ca58fda09628cbcb2275af064/mmh3-5.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65", size = 117438, upload-time = "2025-07-29T07:43:16.865Z" }, + { url = "https://files.pythonhosted.org/packages/a0/08/bfb75451c83f05224a28afeaf3950c7b793c0b71440d571f8e819cfb149a/mmh3-5.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991", size = 120409, upload-time = "2025-07-29T07:43:18.207Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ea/8b118b69b2ff8df568f742387d1a159bc654a0f78741b31437dd047ea28e/mmh3-5.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645", size = 125909, upload-time = "2025-07-29T07:43:19.39Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/168cc0b6a30650032e351a3b89b8a47382da541993a03af91e1ba2501234/mmh3-5.2.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3", size = 135331, upload-time = "2025-07-29T07:43:20.435Z" }, + { url = "https://files.pythonhosted.org/packages/31/05/e3a9849b1c18a7934c64e831492c99e67daebe84a8c2f2c39a7096a830e3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279", size = 110085, upload-time = "2025-07-29T07:43:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/a96bcc306e3404601418b2a9a370baec92af84204528ba659fdfe34c242f/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513", size = 111195, upload-time = "2025-07-29T07:43:23.066Z" }, + { url = "https://files.pythonhosted.org/packages/af/29/0fd49801fec5bff37198684e0849b58e0dab3a2a68382a357cfffb0fafc3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db", size = 116919, upload-time = "2025-07-29T07:43:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/2d/04/4f3c32b0a2ed762edca45d8b46568fc3668e34f00fb1e0a3b5451ec1281c/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667", size = 123160, upload-time = "2025-07-29T07:43:25.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/76/3d29eaa38821730633d6a240d36fa8ad2807e9dfd432c12e1a472ed211eb/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5", size = 110206, upload-time = "2025-07-29T07:43:26.699Z" }, + { url = "https://files.pythonhosted.org/packages/44/1c/ccf35892684d3a408202e296e56843743e0b4fb1629e59432ea88cdb3909/mmh3-5.2.0-cp314-cp314t-win32.whl", hash = "sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7", size = 41970, upload-time = "2025-07-29T07:43:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/b9e4f1e5adb5e21eb104588fcee2cd1eaa8308255173481427d5ecc4284e/mmh3-5.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d", size = 43063, upload-time = "2025-07-29T07:43:28.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455, upload-time = "2025-07-29T07:43:29.563Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "orderly-set" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, + { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pip" +version = "26.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/474d0a8508029286b905622e6929470fb84337cfa08f9d09fbb624515249/platformdirs-4.6.0.tar.gz", hash = "sha256:4a13c2db1071e5846c3b3e04e5b095c0de36b2a24be9a3bc0145ca66fce4e328", size = 23433, upload-time = "2026-02-12T14:36:21.288Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/10/1b0dcf51427326f70e50d98df21b18c228117a743a1fc515a42f8dc7d342/platformdirs-4.6.0-py3-none-any.whl", hash = "sha256:dd7f808d828e1764a22ebff09e60f175ee3c41876606a6132a688d809c7c9c73", size = 19549, upload-time = "2026-02-12T14:36:19.743Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "puremagic" +version = "1.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/7f/9998706bc516bdd664ccf929a1da6c6e5ee06e48f723ce45aae7cf3ff36e/puremagic-1.30.tar.gz", hash = "sha256:f9ff7ac157d54e9cf3bff1addfd97233548e75e685282d84ae11e7ffee1614c9", size = 314785, upload-time = "2025-07-04T18:48:36.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/ed/1e347d85d05b37a8b9a039ca832e5747e1e5248d0bd66042783ef48b4a37/puremagic-1.30-py3-none-any.whl", hash = "sha256:5eeeb2dd86f335b9cfe8e205346612197af3500c6872dffebf26929f56e9d3c1", size = 43304, upload-time = "2025-07-04T18:48:34.801Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860, upload-time = "2026-01-24T05:56:56.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768, upload-time = "2026-01-24T05:56:54.537Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-benchmark" +version = "5.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py-cpuinfo" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-env" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911, upload-time = "2024-09-17T22:39:18.566Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141, upload-time = "2024-09-17T22:39:16.942Z" }, +] + +[[package]] +name = "pytest-httpserver" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/1c/9d1a388fcd3898537b6c9ef06fdf9a42c9b9ffefdb8e3ce50174f4d0211d/pytest_httpserver-1.1.4.tar.gz", hash = "sha256:4d357402ae7e141f3914ed7cd25f3e24746ae928792dad60053daee4feae81fc", size = 8295814, upload-time = "2026-02-09T18:18:00.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/50/7ed2153872d593c14d3a5565f9744d603a3e23967ab3cea0cc1eeb51a1ff/pytest_httpserver-1.1.4-py3-none-any.whl", hash = "sha256:5dc73beae8cef139597cfdaab1b7f6bfe3551dd80965a6039e08498796053331", size = 21653, upload-time = "2026-02-09T18:17:54.772Z" }, +] + +[[package]] +name = "pytest-httpx" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/89/5b12b7b29e3d0af3a4b9c071ee92fa25a9017453731a38f08ba01c280f4c/pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f", size = 54146, upload-time = "2024-11-28T19:16:54.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/ed/026d467c1853dd83102411a78126b4842618e86c895f93528b0528c7a620/pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744", size = 19442, upload-time = "2024-11-28T19:16:52.787Z" }, +] + +[[package]] +name = "pytest-rerunfailures" +version = "16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/04/71e9520551fc8fe2cf5c1a1842e4e600265b0815f2016b7c27ec85688682/pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e", size = 30889, upload-time = "2025-10-10T07:06:01.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/54/60eabb34445e3db3d3d874dc1dfa72751bfec3265bd611cb13c8b290adea/pytest_rerunfailures-16.1-py3-none-any.whl", hash = "sha256:5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86", size = 14093, upload-time = "2025-10-10T07:06:00.019Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "python-daemon" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lockfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/37/4f10e37bdabc058a32989da2daf29e57dc59dbc5395497f3d36d5f5e2694/python_daemon-3.1.2.tar.gz", hash = "sha256:f7b04335adc473de877f5117e26d5f1142f4c9f7cd765408f0877757be5afbf4", size = 71576, upload-time = "2024-12-03T08:41:07.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/3c/b88167e2d6785c0e781ee5d498b07472aeb9b6765da3b19e7cc9e0813841/python_daemon-3.1.2-py3-none-any.whl", hash = "sha256:b906833cef63502994ad48e2eab213259ed9bb18d54fa8774dcba2ff7864cec6", size = 30872, upload-time = "2024-12-03T08:41:03.322Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-whois" +version = "0.9.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/0c/537914eca91ee5ff281309a5ca71da23c0c975cd6658668a44d3fdcf1cc4/python_whois-0.9.6.tar.gz", hash = "sha256:2e6de7b6d70e305a85f4859cd17781ee3f0da3a02a8e94f23cb4cdcd2e400bfa", size = 125107, upload-time = "2025-10-07T04:36:14.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/53/d0ceb3ae30da8e8ec2d9af11050178f3b4114d5aa6a7f7074199db3c806f/python_whois-0.9.6-py3-none-any.whl", hash = "sha256:153261941a4d238b1278a4ca9b5b5e0590ed3b4d0c534ba111c4434d5d339410", size = 116976, upload-time = "2025-10-07T04:36:12.328Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "radixtarget" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/0f/f581bd64b2b1cff364fa94539ee6e99924ff28ceb017ddc819aa7deff9a0/radixtarget-4.2.0.tar.gz", hash = "sha256:c6ba17832edeed75d63ce605327b79a2ed9b24a4a5eae9c1b1419adbdbfaf839", size = 703140, upload-time = "2025-12-16T18:44:00.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/f5/551f8df0744fbed27d42f48e1aae3e8fe8ed4dcbb21310ad0eebca49019b/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa0d14f3c1df46fbd81ddc58df1bc376e6a22610e1491b6fb485b0ca1959977", size = 525953, upload-time = "2025-12-16T18:47:58.819Z" }, + { url = "https://files.pythonhosted.org/packages/66/5f/64a17cc53cdc094e7aa09c84f02891627cd00d0272a09424c8a7360718fa/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cbfafea3ff152c7ed5f9731657caad6e0b3556b6cbb9fbad89b0c483630a2c1", size = 536305, upload-time = "2025-12-16T18:48:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/29/4a/2f91713e7cb651188f31038fcb61a9af8ff1a7e7463ba8819846f0ae6642/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44af119000a99be3851e56d545d75d6e12c683c9966950320cb1a2ccf1333cc0", size = 711324, upload-time = "2025-12-16T18:48:23.324Z" }, + { url = "https://files.pythonhosted.org/packages/8d/91/b07232578100307f1a3ad15b7108823691429968e9b26185d3ac8a587f23/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba8c8349aca622b722b62c766a298883ac0495a259768e8753b2aa4b1f7073cb", size = 561546, upload-time = "2025-12-16T18:48:34.563Z" }, + { url = "https://files.pythonhosted.org/packages/71/8d/bc63c8d89b6092f749db2eb6cbc380dfdda140b82e1cb155a2f09077e837/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b83b17269cbeeb5c87c53f36c850ab35e50e45290b5da14c9f1d5bf93dd4573c", size = 532511, upload-time = "2025-12-16T18:48:55.502Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5a/06c63a9659a21db2043812a276cb61e85343627039dabd1544b772b1200d/radixtarget-4.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0ba767a789a5d7a9b3b6f054bd2b3b2a627db8cbe1de11d837e06f4bad40472", size = 561009, upload-time = "2025-12-16T18:48:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f3/092c899d35889b9e3796400fdcd4e25ee50a1c6e908b010a6e6956efd1b9/radixtarget-4.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b59066e196ed402f30b0df70a243734f4fabd67324bdedec5e130409c55d606a", size = 708927, upload-time = "2025-12-16T18:49:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/6214504bf0fdd1f321d9c9d0e1850b6718cb94c81d49e73e75c7f3e7be1e/radixtarget-4.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:05efd8b2603e61818cdf5d89a43d2a0103c5e3574ab96b60a81b08ec4caa53dc", size = 807385, upload-time = "2025-12-16T18:49:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/27b5f2f392312c81b355a8ee10d14fa15c8dea2db197c5206481d584bc01/radixtarget-4.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7e354cf2e84732b631b87af1fe3555650fc9e21bd9618d425a277ab07a14ff87", size = 772106, upload-time = "2025-12-16T18:49:38.981Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/1cfd3058e0da4d8172d10334e18af0b47317b652c5d33c57188634a3c95a/radixtarget-4.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d65164dff07141276e5ac758ba42df0378727ca51812b18b8e296bb68c1aa2b", size = 736298, upload-time = "2025-12-16T18:50:05.846Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/ff526bfd19b6d1aac758e8c256996eef6162a80594293658f4119ff468bc/radixtarget-4.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed274b85d0bde14c4ba1e9efa753644b8b19170c61d66987d212e2463e14e9af", size = 378903, upload-time = "2025-12-16T18:50:34.057Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d7/f855b8cf09f06e1a5806b6cca93592394ce4e79df2d00a76ae522bbf384b/radixtarget-4.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bad895da09a580c944f050b506d752cf0350c3874bf42f84b517246b5a18649a", size = 497631, upload-time = "2025-12-16T18:49:09.338Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6f/6413fd33eea0b22d2ed0db194bccffbef922c89e002f0d99befebc7354ed/radixtarget-4.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3dfb196f516a665194ad3f22e402a3c1fbbf9a8353a97fd3829273a77c9a0da1", size = 492115, upload-time = "2025-12-16T18:49:04.673Z" }, + { url = "https://files.pythonhosted.org/packages/70/23/e296772b5a811b8fbc2d224880166112c446cecc51b0774c0d71ec59462b/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0253d0852cfb736c3e484c6ac77a401689bcd4dbc1d128f743d90b12360f401f", size = 526063, upload-time = "2025-12-16T18:48:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bc/da800929b5fc9456ef3221a19465806e7b85c0bb97118e8d2d9e27fe51ec/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09b587a22cbf735d790db5bf6a116c0311c7502a568177345c2725d267430212", size = 536300, upload-time = "2025-12-16T18:48:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/6eaecc12148f9998f4d3237cd6ed4c23caf416ca90fa25147e92bf218aa0/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6160c5ac6f7c08c22d5b3a4dc9011e2b822906f157a7dc3ea9921f3a15ad15", size = 711722, upload-time = "2025-12-16T18:48:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3d/54231188743f9aa731a1352b4104cdb48299c8def3750ef56c743a6ba3e9/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe042a589c4045607816dbf79bc41fcd9b421ce93b3e296612945ab46fe97f5a", size = 561534, upload-time = "2025-12-16T18:48:35.613Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8d/fe45c6390bd825279ec57975acf86eca745dab5cae1797a9bc72ed29a6b2/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb243a1ebf4b089e830dcb6d08f96e751de0a116eb522677a42b5e4e9ec0e05c", size = 532615, upload-time = "2025-12-16T18:48:56.612Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/e812eaea1ff3aa7394ed9df30d21e5f8643b7513b303b9be4949f10b0bf1/radixtarget-4.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb82132c07e43ef30e7687849a71c676e533f786373b6673d3e0f796614de6ec", size = 561252, upload-time = "2025-12-16T18:48:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/47de5b14f3fe3be5085267bfef75ba3af25cfe561584c8f275be672144a0/radixtarget-4.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b0df39424c81c946c1c6f11e2389b361644e418bf1bbc6a8b80c23d26607d278", size = 709082, upload-time = "2025-12-16T18:49:15.269Z" }, + { url = "https://files.pythonhosted.org/packages/25/56/18381cc7511db6a98957f83f420d20f3e158fc7e3b6085551ebfcbaf257c/radixtarget-4.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:581e0791229bfc4801af86405b62cb4f3eb41c5c68868151cb6c97b0ef32b685", size = 807210, upload-time = "2025-12-16T18:49:27.884Z" }, + { url = "https://files.pythonhosted.org/packages/58/6a/6d29f2e46be3acb30ea120efaa3e335c723db10f740e5091f2db825e8408/radixtarget-4.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:49797ad5db114e600372184ff7f79e1435613156f4cab67f5b7b32c2ac1cc291", size = 772474, upload-time = "2025-12-16T18:49:40.404Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/2b4962600262e8137c0223aea7f8ac5979143735c30d371fce4fec123b68/radixtarget-4.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eb702384f5e90e1b911f8d4f314e59030759a519fc14bcb2e37135091dd091f", size = 736455, upload-time = "2025-12-16T18:50:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3f/d171081c15c2113a10a9b9a35eceea5c421a6d55d212fb3c5e6b112aae9f/radixtarget-4.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:0c515fcd99dd268f6b4e1eb21bbf14b6818c9c1317496c06bc3fd47790173b72", size = 378916, upload-time = "2025-12-16T18:50:36.406Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/2dd66769df40ceed730ed50d00b06430c15dfe5ab7f02d07ee313826d9b3/radixtarget-4.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d9d305119875ffda3af4e4cbb60ab3a3f44a618276edb6154de58bfb7fa2ce3f", size = 498895, upload-time = "2025-12-16T18:49:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/e1/21/eaf70a8b7d5be07cb0722a495b7da227f9b6979e246c08af37174d70e972/radixtarget-4.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5ddd4cee0a3e6640602f6695469b0ce86fb669eefe40db56a66982368ba5350", size = 490903, upload-time = "2025-12-16T18:49:05.829Z" }, + { url = "https://files.pythonhosted.org/packages/0c/27/a34161399043180158645aed3045c1f17b156eeb1e4518794cd5254ca568/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b23e10d2452f29fe09d3095ded30fdbc221b1acd10360e2a5f81856a756c4c73", size = 524748, upload-time = "2025-12-16T18:48:01.4Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f8/0219eb4a010b90aeaf911ba030ac29189f9a71007cabbd1c7f1d63252562/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75903eda31182dedc7e3342a8879f79709138829c20a8258279a4c7e78156c1d", size = 536507, upload-time = "2025-12-16T18:48:12.663Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/e7b6b3c00b380c6567551009e08662516bf653a2a7282a2b365921ca51ac/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a43eca0711a743dd897a5fef0aa513798ad12fae19553aaf24e18fd21fe96597", size = 708677, upload-time = "2025-12-16T18:48:25.691Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4a/fc0c5eacaa977a6496967282c154da8b5798f5e8c0c705fea773e8a3ff58/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec3ff6c8b447891921f5ecbedc8569053b16c0cc356fed731b99c470c20aebab", size = 560847, upload-time = "2025-12-16T18:48:36.773Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/0f952cb79dd99f90d750c39635206faa30f39b09e5cfdb3ae2f377e80044/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d1119f57e74307abda2fd1a23e83c0338373214a539ab1286a88dda55b91f1", size = 531507, upload-time = "2025-12-16T18:48:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/87/32/e9120cda5cd05e99835d7664e9fa4dddbf5fb57fc07d80ad46453ab38d7a/radixtarget-4.2.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:76d46168ce9acc65abbe553b435ea113c65762a47148a8406142a2176c5e53bb", size = 461830, upload-time = "2025-12-16T18:43:59.227Z" }, + { url = "https://files.pythonhosted.org/packages/76/48/aeec382df32d2ad85905cf46096a1bfb6c067a7438cba06a8c7f549792dc/radixtarget-4.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62eae4c069123ebd933b935339b4b19c477308ab308262e4a765e4dd7443077a", size = 559835, upload-time = "2025-12-16T18:48:48.951Z" }, + { url = "https://files.pythonhosted.org/packages/f4/81/8c00262b004228163104c2996a3629b6cf828b61b0282ab391b67a0d29ee/radixtarget-4.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877f6a960a7309dcd9c2eb10fed4ca7e03c117fafbda8659e52bb082466ebb56", size = 707968, upload-time = "2025-12-16T18:49:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/c4707fa238d6fa2b2eb16f83db4b7840a51cfeab667704980836158d4e52/radixtarget-4.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d92109f94a916aac2fc04ee4c8f5a3f1a3ecb60df774d1febb2a8e1aa22d0694", size = 806824, upload-time = "2025-12-16T18:49:29.073Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a6/6e9b8d2f50e9dd2402e568f03fe305767a4018764cb4903aa04c730fc243/radixtarget-4.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e70a00bc5f27d49c2c6aeda47adb776f639d503f726ef6cd484a917aa603d2a2", size = 771612, upload-time = "2025-12-16T18:49:56.618Z" }, + { url = "https://files.pythonhosted.org/packages/34/73/2a57c889a5bb209f265a6cb9734f119a69dc760684b586458de86de7fed2/radixtarget-4.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f38ef0281e4af7a5dc41a040b2adbe45202a2c904666d9ae141d53869f33db02", size = 735221, upload-time = "2025-12-16T18:50:08.746Z" }, + { url = "https://files.pythonhosted.org/packages/2f/fe/bfc926131d4d950aec8657aa0d7f7c6eff0d45690375245a61dc292abebd/radixtarget-4.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:dfd80f33aa21bbf30eba4a5482bdf4d5da7d6f231b6a1f7d55a99887cdc118d2", size = 379211, upload-time = "2025-12-16T18:50:37.713Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/becbfd7ecacee757c5cd41560ee2c044a2e8e19c0ad273e44f3ab9f38b02/radixtarget-4.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b220ac0837d70837a59a1f1030c671ce4cf706ea9402268fea2effbba9a9e73", size = 498485, upload-time = "2025-12-16T18:49:11.679Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e2/46a645929ea18c24f4690da321f3cf29f9be328e021b0a513c55a6dfdffa/radixtarget-4.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3dcac7b61bcbad700b0347c3aa930dcc85dc4cfc3d9432ce977737d11972b71c", size = 491313, upload-time = "2025-12-16T18:49:07.009Z" }, + { url = "https://files.pythonhosted.org/packages/50/68/1a5aaf605e2d194619931f95cf60c2d20e97437ff9a340aa765cfca08b2a/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1a9ef464e354ff4cb6e9b81cbe7cae61614e7d08ed7c380b2345f3d87faa1d3", size = 524293, upload-time = "2025-12-16T18:48:02.827Z" }, + { url = "https://files.pythonhosted.org/packages/de/81/dec202a939bf0b066a3ad4cf8308948b401508575fc6c520400a09289fe9/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96a0c97c84c347d3628ee825711b08a351b0ae8e1f7c37435f562fd64fe33ca9", size = 536159, upload-time = "2025-12-16T18:48:15.901Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3b/3a634622a98c6db58e27f4ff49efa9d25bbdd73a9302e47ff3ea0d0d5082/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a5f86e9b8805959ee7f66bd6a82d6a67e2087ece27a89ea135de5e80fe1aebb", size = 708628, upload-time = "2025-12-16T18:48:27.002Z" }, + { url = "https://files.pythonhosted.org/packages/b2/56/c347a34ded989e2ebcf388720d437e48b67c8c8ea4fcc2ed0579ccc68f58/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2ed8149e6effffa647a2c0341fba0a97d596de37351d28a354dd76bf6840292", size = 560428, upload-time = "2025-12-16T18:48:38.009Z" }, + { url = "https://files.pythonhosted.org/packages/81/8b/e462f93a8dd8c48e7e9c511aeebdcf924e1476e9ceb53c85e91e049e3d71/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a81a575e771e360556c404352b3868abe3717265c53f243edeb6dc84416bb0b", size = 531269, upload-time = "2025-12-16T18:48:59.377Z" }, + { url = "https://files.pythonhosted.org/packages/9d/92/0709d70f497a0360e9a7db0387ba3dfa2ce43bcb9ecd0a8505a715d88106/radixtarget-4.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17d6254b9449b12e249107340eb1b9f28cac224070cd075a5468e9683e974de3", size = 559408, upload-time = "2025-12-16T18:48:50.429Z" }, + { url = "https://files.pythonhosted.org/packages/30/a2/9817987f6b98cb6851b5744b9e86a917c3ca0775c8e8797c2edd85148d70/radixtarget-4.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:548cc66578feb9a5a4f6af6caab53e68bf5351f3d4361ab8d4f3646281a7fbd5", size = 707551, upload-time = "2025-12-16T18:49:18.38Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/ac4c3ec42c2d12d3a1ca26b95bb652436957f7708562cca506976a18e2b3/radixtarget-4.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a428ee5cd0ba7dabdd24311a697dee79a14a1719de1dad60e98cf1e5f2bdb083", size = 806347, upload-time = "2025-12-16T18:49:30.627Z" }, + { url = "https://files.pythonhosted.org/packages/c9/af/46f6d575a59dd60b686064f0bf62c97f78f5a797ef59505c78c54ef2f74b/radixtarget-4.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82ed101dc5d8a7b3ec12de59b788683d3b40b862a5f83a4c094ad22a937b9f30", size = 771144, upload-time = "2025-12-16T18:49:57.992Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ce/d1e83431bc68ae0cabdbc373cb985de5be03e3856baa0f2086c18f3be9d6/radixtarget-4.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2770e86a92220b08706b647cd00639c893f0c51cab413d748e129cfc2c49e362", size = 734990, upload-time = "2025-12-16T18:50:10.119Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/04996087bcba40b19094ce21593b53041d4bfc1bd1712f5416459cdb5b48/radixtarget-4.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:d5b99dca0b6177318396dc7170681f384241819491d0695eb8fc13e9bde5cf61", size = 378947, upload-time = "2025-12-16T18:50:39.214Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b4/1b5d3a0cd7805edc15e03f6281ac7aeb3dd030dba77c9513e196949f15e1/radixtarget-4.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:653378321743c3e79471298e03929a345fff9450abba0f1495638355d1049d95", size = 522379, upload-time = "2025-12-16T18:48:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/83d3cf51a87ad6400d75a3ac25dd1c674fecdd9b05d3831768f7c8ffa53b/radixtarget-4.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f515860a4a5e18022a7bbafdf36dff82b3614ede92008d52f1aa1664220dbd5", size = 533382, upload-time = "2025-12-16T18:48:16.982Z" }, + { url = "https://files.pythonhosted.org/packages/fb/40/ecb9eae445401566b3e9b61dec4c262ae19b98ae4c91aac4806665535e72/radixtarget-4.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fca34e9c48e5a1c6d81849b185591a8b8e256c3e755a3d815951f5311880b73", size = 707425, upload-time = "2025-12-16T18:48:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7b/ae9ff987841fc85663f3cd4377b7e259353a9f2c75f187706143ad31d105/radixtarget-4.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7b939a7f1bd17f1e2257313a90ebb19ba10e7e59a0c5e5e113fedb882966684", size = 558296, upload-time = "2025-12-16T18:48:39.343Z" }, + { url = "https://files.pythonhosted.org/packages/35/b9/057d2d88941610d3492e104f6c656cd2da9d323325e27ced178f62044565/radixtarget-4.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:05915257e8ac3ee05812e411f1ae2c62851fd4986d84f8c26f44bc33a3fe1043", size = 704696, upload-time = "2025-12-16T18:49:19.645Z" }, + { url = "https://files.pythonhosted.org/packages/71/b5/871e0339af40eddee2373ca6eb9773f3bdc2757cdeb4347386376f4bc241/radixtarget-4.2.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e67f5cd923de5d7aeb0688105cfcc2feedcfd3ba05310585ac0fa63f8ab2f634", size = 803127, upload-time = "2025-12-16T18:49:31.902Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/be0c086ac255fe4d1ab75bdb5a5929233640408104843ca9e9b03faf3a7b/radixtarget-4.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c9529b9fd3f0bed32b64f148859ba7832b75c26e582e4c1cc7586c315454c5b6", size = 768508, upload-time = "2025-12-16T18:49:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/0e720d6022b6d9ace940798da70c3735c6b879698d83af60fb2282d81f6a/radixtarget-4.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4f90f1a8270600f3bdc2173757623e85e928815b091e1e145afa3032c26d40a3", size = 733015, upload-time = "2025-12-16T18:50:11.859Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/a595055e758b5e226bf234eb86f3ec5fbc1976e38e915d897d76fc31b84d/radixtarget-4.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:741c7d8833a88bd1305253acbe558920ee818be27fa1c1aea0e0fb3564e0936e", size = 495466, upload-time = "2025-12-16T18:49:12.882Z" }, + { url = "https://files.pythonhosted.org/packages/52/bd/1ae6b8e9609dffa00d3e2a1c4fa5f474c12eb60f2564a65e971928718dbd/radixtarget-4.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:33357489b71862dc63bf029a7803f5aee80466c2ca83b2120d8dea921d35f1e4", size = 487773, upload-time = "2025-12-16T18:49:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/75/b6/72eb662c79e03a1f701ef0cad0272e6e90aab062a5c7ba2dd06322ebecbf/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:424611144171c5bff08f123d1b579800d0a7a987f1f28a065ecbfa40a4ff8a21", size = 523213, upload-time = "2025-12-16T18:48:05.016Z" }, + { url = "https://files.pythonhosted.org/packages/a7/44/9efe558f4bcaeee940568444cb87673b471f006e349520dcb730437fe0d2/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79dc672f10299d5e95522382f4965d26e484916dba4ff44e4ab77e6d4fb80b9d", size = 533440, upload-time = "2025-12-16T18:48:18.18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f4/32035a5f9e55258611ecd16c7cb7f63e8ed8432b9f60b85f77950270aebc/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84797ea9544bad786f1c0a233d5081418a76217821345b0f23e56fce713b7659", size = 706516, upload-time = "2025-12-16T18:48:29.573Z" }, + { url = "https://files.pythonhosted.org/packages/7d/49/8ed31f8e089b3aeb9ad14936fd0881530bd963fe6c64ebb40742e1d4ca8f/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77c405240eb624970fb4194d498c23833fc404361dea167e614ca27b25f1b96", size = 559263, upload-time = "2025-12-16T18:48:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f2/f71acd1ce0b4e2bb3c6fa71b60fe746676e39c205f7db01498edef1e1af9/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ed5064530dc2185f61f7e6a05b62bd55e20b764eb4c0b156f1c1dd06a538ec5", size = 529125, upload-time = "2025-12-16T18:49:00.662Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3e/17ccc7ba7d7cda61411ec0166b19a98b54f924431e005811db4a081fd55f/radixtarget-4.2.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4f54893bd7ac2985c470b4edeea894661b762b4805e0c585a0a94a1d6155ea7b", size = 556447, upload-time = "2025-12-16T18:48:51.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/c7/25987424b0e3e0c72ec7367b9403bca853fbe570328528223d0ca57494e1/radixtarget-4.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3a57aea7d709049d2f55d633dda684d87316b962b7bf4a00fa2d3063023e8dd8", size = 705990, upload-time = "2025-12-16T18:49:20.854Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/1f153fb4d57343baa978ffbff99c9f853e7959f966bb9d40308ebac8fffc/radixtarget-4.2.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:233a39a73e0c2b3341ffea438d8d4722d8b5287edaa5d5ddb778ced9d437c9d2", size = 803275, upload-time = "2025-12-16T18:49:33.406Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/45f6b55bd57b8eee6b5a5d8882255364c3f7e1aabd45521178109da59fde/radixtarget-4.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d75bf219fb61d0149164926c3901aa8bd0587ac63444edae846e650cd4af2a33", size = 768779, upload-time = "2025-12-16T18:50:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/37/5b/92dd5e447f9ff20e4462f3a4ba37d1903f283cfb8b6ec2036a14899fefac/radixtarget-4.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9041444da3bb83c1c6e82c44b33808490390ecbcacb35010f27248fd75d7ed70", size = 732662, upload-time = "2025-12-16T18:50:28.222Z" }, + { url = "https://files.pythonhosted.org/packages/51/59/987a5ee67627e8f4a8dd016d9c43fa3113a6991f0460a422e839fb93a460/radixtarget-4.2.0-cp314-cp314-win32.whl", hash = "sha256:6d3353b8e93c43841c94d1f1de8447cf4843acda3c0376aebc4c0249482e1869", size = 369629, upload-time = "2025-12-16T18:51:05.531Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/bc78cfb2e46b16c265cf10fc746eb05d39079f04051c35554f74a4881429/radixtarget-4.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e6e8db75a7ba938e4fd4ff03f6768c0381ecf4e3d26d6252cf9f60e0e572d60b", size = 376294, upload-time = "2025-12-16T18:51:02.813Z" }, + { url = "https://files.pythonhosted.org/packages/0b/37/5334f46e8ebbfb533735624c2645276401feee66ddf3413e75376925c1b1/radixtarget-4.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:461936708cd075ea37946eef58b12fd198b031965c6e0c25f801b9fe3cf87974", size = 523072, upload-time = "2025-12-16T18:48:06.194Z" }, + { url = "https://files.pythonhosted.org/packages/30/96/9bf4388abc4a0f9afbaa0fd55d77b9fb206c9c43dcc49fe7b138a6f031ab/radixtarget-4.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6221e7ee00df05c6c2d28e86096d441e8f6a81f12edbfe38422f88b587fef891", size = 534222, upload-time = "2025-12-16T18:48:19.315Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a7/f0f24ddd254d2f86a2ff13f37d9886df0e0c4500ff4f481d865d8a482aa4/radixtarget-4.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1e55858a955ceb6946cebc5a1664cdfff682cf380e03015ac22f40a73785079", size = 706790, upload-time = "2025-12-16T18:48:30.763Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d2/c28bc32796f703fb2ef78af374bd63c1e85cc19dc2dd1476f244646c3af4/radixtarget-4.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:773d317934ed1b3c86f808f80a4af4d3756b21a268173ad1ffd9b7505ec3fc73", size = 558637, upload-time = "2025-12-16T18:48:41.736Z" }, + { url = "https://files.pythonhosted.org/packages/74/9e/2ebde415856c066f9e51a07096153f2f9b9d130af4c28e051600976c5d88/radixtarget-4.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7bab59aac38f370c10845b7a1fb4335a9bd2bc3978cb81ce60e534f97373f86", size = 705271, upload-time = "2025-12-16T18:49:22.497Z" }, + { url = "https://files.pythonhosted.org/packages/5f/29/f23905191087e3cbc38c7328ad985daeec6753fc7812fab0b4525600e316/radixtarget-4.2.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0bf49f4524f2b309e4d164f405336df116f257a80074cee669c5b5b51e8eb484", size = 804030, upload-time = "2025-12-16T18:49:34.625Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ba/031cbbfdc807acb66e8183cf47ed7bc8a34c0ff8065ab52b913e86f7b44d/radixtarget-4.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:01dc45a2e24565f4f269bc724024fbefa9a12bd4f2b9c4d3542caa45ea3e4eb7", size = 768802, upload-time = "2025-12-16T18:50:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/115fb6ab50682bb041e15dcb9c72e35e3868edc297f55799aa4404012c54/radixtarget-4.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0a368e059ffba6e786f5552eb9b8638ad90195643074830314d8cec6f23f157c", size = 733430, upload-time = "2025-12-16T18:50:29.94Z" }, + { url = "https://files.pythonhosted.org/packages/af/9c/e79af1c408b008cde3054e408aec2f78ead5d754c96e392117ba5ef24cb2/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:858831b2b369d8973ddd927bd870c353a4386d0a517ab3855a2fc73f2f1b61cc", size = 527144, upload-time = "2025-12-16T18:48:09.392Z" }, + { url = "https://files.pythonhosted.org/packages/05/c8/2584c148a34a5df38024ef3296479b7a86de0b70d44956fcc395e72ad8aa/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d5a75d34f55009a4513dcb396b638ba7dba672d5eb107b7e9f0f0262b98775c4", size = 537302, upload-time = "2025-12-16T18:48:21.896Z" }, + { url = "https://files.pythonhosted.org/packages/62/81/0ebab51c21e24331b78471d1e10cf79b1f5df13a3f214d2d64db1be36111/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92f908ec996f49686a35dec20d47a98f0b24648c5eacc24c1969cbb13595107c", size = 712638, upload-time = "2025-12-16T18:48:33.441Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/1e7cb866e724017a102f383ff0715424d77a76242b3793c562d1b2e13f3f/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2499a664b78bca3b0536d8ac3d57dabdba1652c9c9e746979a743873f4edc19", size = 563931, upload-time = "2025-12-16T18:48:44.595Z" }, + { url = "https://files.pythonhosted.org/packages/97/39/a88a4d1f6d57fda78a9481e0955674ce079d4e5758d35e7e3e876b465aee/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482b5b5c317bafb49fc1d9e5a648bddabb38dddec2be336545146d743d09622d", size = 533809, upload-time = "2025-12-16T18:49:03.476Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1e/9b2c183e5d5def426945d3364cbb5c62968e5ed0a21d56eefd58b7d22cb0/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886001cc5679657cc643aa94858ef1a989bdfb168f60122222a25d5c0b11e8d5", size = 562933, upload-time = "2025-12-16T18:48:54.038Z" }, + { url = "https://files.pythonhosted.org/packages/93/56/2e4b3ffc43aaefadce4b03a0ad525cd8663e520e63232174450da3a68177/radixtarget-4.2.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f230e01ee9333ca2202ced3a1e0d6ee24cdad698283b32c453ca1846762992f", size = 710660, upload-time = "2025-12-16T18:49:25.052Z" }, + { url = "https://files.pythonhosted.org/packages/21/75/80f1460e98697f90387782644ae83f0b6f14bde4d8bb24c50562bd95db8b/radixtarget-4.2.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:4ef1b02bf1fb907e18403cb424b74b6ec05df6b103aba5e14768b6b208fd2e12", size = 808362, upload-time = "2025-12-16T18:49:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dc/9ad890f8c48b1997a8b1a258c421a8014f963c7a883932a6a043d6e56670/radixtarget-4.2.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8160247559a35a4df41aa50c786afd0dbbd983e3071e815017e3fa62a64bc72", size = 773601, upload-time = "2025-12-16T18:50:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/bb/47/a1bf30b4a047bfa6a1f97a679ebd5322d67cb16fc96cd9cddb2273d2cf10/radixtarget-4.2.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c1f6d83716242d7b7a8463dca7d77f2b9be0edb7ea0f467917ee87e46ef658a6", size = 738037, upload-time = "2025-12-16T18:50:32.41Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, + { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-file" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/5dc70102e4d337063452c82e1f0d95e39abfe67aa222ed8a5ddeb9df8de8/requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576", size = 6967, upload-time = "2025-10-20T18:56:42.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d5/de8f089119205a09da657ed4784c584ede8381a0ce6821212a6d4ca47054/requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2", size = 4514, upload-time = "2025-10-20T18:56:41.184Z" }, +] + +[[package]] +name = "resolvelib" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/10/f699366ce577423cbc3df3280063099054c23df70856465080798c6ebad6/resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", size = 21065, upload-time = "2023-03-09T05:10:38.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194, upload-time = "2023-03-09T05:10:36.214Z" }, +] + +[[package]] +name = "resolvelib" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/14/4669927e06631070edb968c78fdb6ce8992e27c9ab2cde4b3993e22ac7af/resolvelib-1.2.1.tar.gz", hash = "sha256:7d08a2022f6e16ce405d60b68c390f054efcfd0477d4b9bd019cc941c28fad1c", size = 24575, upload-time = "2025-10-11T01:07:44.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/23/c941a0d0353681ca138489983c4309e0f5095dfd902e1357004f2357ddf2/resolvelib-1.2.1-py3-none-any.whl", hash = "sha256:fb06b66c8da04172d9e72a21d7d06186d8919e32ae5ab5cdf5b9d920be805ac2", size = 18737, upload-time = "2025-10-11T01:07:43.081Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, +] + +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/48/fb401ec8c4953d519d05c87feca816ad668b8258448ff60579ac7a1c1386/setproctitle-1.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf555b6299f10a6eb44e4f96d2f5a3884c70ce25dc5c8796aaa2f7b40e72cb1b", size = 18079, upload-time = "2025-09-05T12:49:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/c2b0333c2716fb3b4c9a973dd113366ac51b4f8d56b500f4f8f704b4817a/setproctitle-1.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:690b4776f9c15aaf1023bb07d7c5b797681a17af98a4a69e76a1d504e41108b7", size = 13099, upload-time = "2025-09-05T12:49:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f8/17bda581c517678260e6541b600eeb67745f53596dc077174141ba2f6702/setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c", size = 31793, upload-time = "2025-09-05T12:49:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/27/d1/76a33ae80d4e788ecab9eb9b53db03e81cfc95367ec7e3fbf4989962fedd/setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3", size = 32779, upload-time = "2025-09-05T12:49:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/59/27/1a07c38121967061564f5e0884414a5ab11a783260450172d4fc68c15621/setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f", size = 34578, upload-time = "2025-09-05T12:49:13.393Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d4/725e6353935962d8bb12cbf7e7abba1d0d738c7f6935f90239d8e1ccf913/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64", size = 32030, upload-time = "2025-09-05T12:49:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/e4677ae8e1cb0d549ab558b12db10c175a889be0974c589c428fece5433e/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5", size = 33363, upload-time = "2025-09-05T12:49:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/55/d4/69ce66e4373a48fdbb37489f3ded476bb393e27f514968c3a69a67343ae0/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381", size = 31508, upload-time = "2025-09-05T12:49:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5a/42c1ed0e9665d068146a68326529b5686a1881c8b9197c2664db4baf6aeb/setproctitle-1.3.7-cp310-cp310-win32.whl", hash = "sha256:9e803d1b1e20240a93bac0bc1025363f7f80cb7eab67dfe21efc0686cc59ad7c", size = 12558, upload-time = "2025-09-05T12:49:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fe/dd206cc19a25561921456f6cb12b405635319299b6f366e0bebe872abc18/setproctitle-1.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:a97200acc6b64ec4cada52c2ecaf1fba1ef9429ce9c542f8a7db5bcaa9dcbd95", size = 13245, upload-time = "2025-09-05T12:49:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" }, + { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104, upload-time = "2025-09-05T12:49:54.416Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982, upload-time = "2025-09-05T12:49:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150, upload-time = "2025-09-05T12:49:58.025Z" }, + { url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463, upload-time = "2025-09-05T12:49:59.424Z" }, + { url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848, upload-time = "2025-09-05T12:50:01.107Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544, upload-time = "2025-09-05T12:50:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235, upload-time = "2025-09-05T12:50:16.89Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058, upload-time = "2025-09-05T12:50:02.501Z" }, + { url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072, upload-time = "2025-09-05T12:50:03.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490, upload-time = "2025-09-05T12:50:04.948Z" }, + { url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267, upload-time = "2025-09-05T12:50:06.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376, upload-time = "2025-09-05T12:50:07.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963, upload-time = "2025-09-05T12:50:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550, upload-time = "2025-09-05T12:50:10.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727, upload-time = "2025-09-05T12:50:12.032Z" }, + { url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549, upload-time = "2025-09-05T12:50:13.074Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243, upload-time = "2025-09-05T12:50:14.146Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, + { url = "https://files.pythonhosted.org/packages/34/8a/aff5506ce89bc3168cb492b18ba45573158d528184e8a9759a05a09088a9/setproctitle-1.3.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb440c5644a448e6203935ed60466ec8d0df7278cd22dc6cf782d07911bcbea6", size = 12654, upload-time = "2025-09-05T12:51:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/5b6f2faedd6ced3d3c085a5efbd91380fb1f61f4c12bc42acad37932f4e9/setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2", size = 14284, upload-time = "2025-09-05T12:51:18.393Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c0/4312fed3ca393a29589603fd48f17937b4ed0638b923bac75a728382e730/setproctitle-1.3.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6f268caeabb37ccd824d749e7ce0ec6337c4ed954adba33ec0d90cc46b0ab78", size = 13282, upload-time = "2025-09-05T12:51:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" }, + { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "socksio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "tabulate" +version = "0.8.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/53/afac341569b3fd558bf2b5428e925e2eb8753ad9627c1f9188104c6e0c4a/tabulate-0.8.10.tar.gz", hash = "sha256:6c57f3f3dd7ac2782770155f3adb2db0b1a269637e42f27599925e64b114f519", size = 60154, upload-time = "2022-06-21T16:26:42.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/4e/e5a13fdb3e6f81ce11893523ff289870c87c8f1f289a7369fb0e9840c3bb/tabulate-0.8.10-py3-none-any.whl", hash = "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc", size = 29068, upload-time = "2022-06-21T16:26:37.943Z" }, +] + +[[package]] +name = "tldextract" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "idna" }, + { name = "requests" }, + { name = "requests-file" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/7b/644fbbb49564a6cb124a8582013315a41148dba2f72209bba14a84242bf0/tldextract-5.3.1.tar.gz", hash = "sha256:a72756ca170b2510315076383ea2993478f7da6f897eef1f4a5400735d5057fb", size = 126105, upload-time = "2025-12-28T23:58:05.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/42/0e49d6d0aac449ca71952ec5bae764af009754fcb2e76a5cc097543747b3/tldextract-5.3.1-py3-none-any.whl", hash = "sha256:6bfe36d518de569c572062b788e16a659ccaceffc486d243af0484e8ecf432d9", size = 105886, upload-time = "2025-12-28T23:58:04.071Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "unidecode" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/4f/f9fdac7cf6dd79790eb165639b5c452ceeabc7bbabbba4569155470a287d/uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302", size = 82001, upload-time = "2025-12-21T13:05:17.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/25/db2b1c6c35bf22e17fe5412d2ee5d3fd7a20d07ebc9dac8b58f7db2e23a0/uvicorn-0.39.0-py3-none-any.whl", hash = "sha256:7beec21bd2693562b386285b188a7963b06853c0d006302b3e4cfed950c9929a", size = 68491, upload-time = "2025-12-21T13:05:16.291Z" }, +] + +[[package]] +name = "verspec" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] + +[[package]] +name = "wordninja" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/15/abe4af50f4be92b60c25e43c1c64d08453b51e46c32981d80b3aebec0260/wordninja-2.0.0.tar.gz", hash = "sha256:1a1cc7ec146ad19d6f71941ee82aef3d31221700f0d8bf844136cf8df79d281a", size = 541572, upload-time = "2019-08-10T02:16:54.944Z" } + +[[package]] +name = "xmltodict" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce52582683ce50980bcadbc4fa5143b9f2b19ab99958f/xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", size = 51942, upload-time = "2024-10-16T06:10:29.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981, upload-time = "2024-10-16T06:10:27.649Z" }, +] + +[[package]] +name = "xmltojson" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "xmltodict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/bd/7ff42737e3715eaf0e46714776c2ce75c0d509c7b2e921fa0f94d031a1ff/xmltojson-2.0.3.tar.gz", hash = "sha256:68a0022272adf70b8f2639186172c808e9502cd03c0b851a65e0760561c7801d", size = 7069, upload-time = "2024-10-20T18:08:17.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3c/80df27969bfbb84425886dd4aaa71875807badd442af65ae7d652592e8ce/xmltojson-2.0.3-py3-none-any.whl", hash = "sha256:1b68519bd14fbf3e28baa630b8c9116b5d3aa8976648f277a78ae3448498889a", size = 7811, upload-time = "2024-10-20T18:08:16.334Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, + { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, + { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "yara-python" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/12/73703b53de2d3aa1ead055d793035739031793c32c6b20aa2f252d4eb946/yara_python-4.5.2.tar.gz", hash = "sha256:9086a53c810c58740a5129f14d126b39b7ef61af00d91580c2efb654e2f742ce", size = 550836, upload-time = "2025-05-02T11:17:48.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/77/8576d9ad375d396aabd87cf2510d3696440d830908114489a1e72df0e8f9/yara_python-4.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20aee068c8f14e8ebb40ebf03e7e2c14031736fbf6f32fca58ad89d211e4aaa0", size = 402000, upload-time = "2025-05-02T11:16:28.228Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ca/9696ebaa5b345aa1d670b848af36889f01bea79520598e09d2c62c5e19ad/yara_python-4.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9899c3a80e6c543585daf49c5b06ba5987e2f387994a5455d841262ea6e8577c", size = 208581, upload-time = "2025-05-02T11:16:30.205Z" }, + { url = "https://files.pythonhosted.org/packages/07/08/e3e6c3641b5713a6d9628ed654ee1b69e215bafc2e91b9ce9dc24a7a5215/yara_python-4.5.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:399bb09f81d38876a06e269f68bbe810349aa0bb47fe79866ea3fc58ce38d30f", size = 2471737, upload-time = "2025-05-27T12:41:53.176Z" }, + { url = "https://files.pythonhosted.org/packages/7c/19/140dc9e0ea3b27b00a239a7bd4c839342da9b5326551f2b0607526aca841/yara_python-4.5.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:c78608c6bf3d2c379514b1c118a104874df1844bf818087e1bf6bfec0edfd1aa", size = 208855, upload-time = "2025-05-27T12:41:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/45/a9/88997ec3d6831d436e4439be4197cd70c764b9fbaf1ef904a2dba870c920/yara_python-4.5.2-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:f25db30f8ae88a4355e5090a5d6191ee6f2abfdd529b3babc68a1faeba7c2ac8", size = 2478386, upload-time = "2025-05-27T12:41:56.691Z" }, + { url = "https://files.pythonhosted.org/packages/6e/15/f6e79e3b70bff4c11e13dfe833f6e28bdd4b3674b51e05008821182918e5/yara_python-4.5.2-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:f2866c0b8404086c5acb68cab20854d439009a1b02077aca22913b96138d2f6a", size = 209299, upload-time = "2025-05-27T12:41:58.149Z" }, + { url = "https://files.pythonhosted.org/packages/da/f9/54bb72a4c8d79c22b211bb30065281de785ee638fa3e5856b6c7a8fd60e5/yara_python-4.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fc5abddf8767ca923a5a88b38b8057d4fab039323d5c6b2b5be6cba5e6e7350", size = 2239744, upload-time = "2025-05-02T11:16:31.793Z" }, + { url = "https://files.pythonhosted.org/packages/8d/13/18e66baf4d6220692d4e8957796386524dc32053ccd8118dfb93c241afb2/yara_python-4.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc2216bc73d4918012a4b270a93f9042445c7246b4a668a1bea220fbf64c7990", size = 2322426, upload-time = "2025-05-02T11:16:33.158Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c6/571f8d5dcd682038238a50f0101572cbecee50e20e51bf0de2f75bf00723/yara_python-4.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5558325eb7366f610a06e8c7c4845062d6880ee88f1fbc35e92fae333c3333c", size = 2326899, upload-time = "2025-05-02T11:16:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f7/f12b796841995131515abef4ae2b6e9a6ac2dc9f397d3e18d77a9a607b5f/yara_python-4.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a293e30484abb6c137d9603fe899dfe112c327bf7a75e46f24737dd43a5e44", size = 2945990, upload-time = "2025-05-02T11:16:36.155Z" }, + { url = "https://files.pythonhosted.org/packages/24/fa/82fc55fdfc4c2e8fe94495063d33eafccacc7b3afd3122817197a52c3523/yara_python-4.5.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ff1e140529e7ade375b3c4c2623d155c93438bd56c8e9bebce30b1d0831350d", size = 2416586, upload-time = "2025-05-02T11:16:37.666Z" }, + { url = "https://files.pythonhosted.org/packages/e3/23/6b9f097bcfb6e47da068240034422a543b6c55ef32f3500c344ddbe0f1da/yara_python-4.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:399f484847d5cb978f3dd522d3c0f20cbf36fe760d90be7aaeb5cf0e82947742", size = 2636130, upload-time = "2025-05-02T11:16:40.042Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d6/01d7ff8281e10b8fced269bf022d471d70cc7213b055f48dbae329246c52/yara_python-4.5.2-cp310-cp310-win32.whl", hash = "sha256:ef499e273d12b0119fc59b396a85f00d402b103c95b5a4075273cff99f4692df", size = 1447051, upload-time = "2025-05-02T11:16:41.519Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0d/5a8b3960af506f62391568b27a4d809d0f85366d3f9edd02952e75757868/yara_python-4.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:dd54d92c8fe33cc7cd7b8b29ac8ac5fdb6ca498c5a697af479ff31a58258f023", size = 1825447, upload-time = "2025-05-02T11:16:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e0/a52e0e07bf9ec1c02a3f2136111a8e13178a41a6e10f471bcafea5e0cdb5/yara_python-4.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:727d3e590f41a89bbc6c1341840a398dee57bc816b9a17f69aed717f79abd5af", size = 402007, upload-time = "2025-05-02T11:16:44.457Z" }, + { url = "https://files.pythonhosted.org/packages/12/92/e76cab3da5096a839187a8e42f94cb960be179d6fba2af787b1fd8c7f7aa/yara_python-4.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5657c268275a025b7b2f2f57ea2be0b7972a104cce901c0ac3713787eea886e", size = 208581, upload-time = "2025-05-02T11:16:45.806Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cf/73759180b5d3ccb0ba18712a4bce7e3aee88935ccf76d8fc954dff89947a/yara_python-4.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4bcfa3d4bda3c0822871a35dd95acf6a0fe1ab2d7869b5ae25b0a722688053a", size = 2241764, upload-time = "2025-05-02T11:16:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/dd/64/275394c7ed93505926bb6f17e85abdf36efc2f1dc5c091c8f674adb1ec02/yara_python-4.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d6d7e04d1f5f64ccc7d60ff76ffa5a24d929aa32809f20c2164799b63f46822", size = 2323680, upload-time = "2025-05-02T11:16:48.504Z" }, + { url = "https://files.pythonhosted.org/packages/6a/84/1753c2c0e5077e4e474928617a245576b61892027afa851f1972905e842a/yara_python-4.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d487dcce1e9cf331a707e16a12c841f99071dcd3e17646fff07d8b3da6d9a05c", size = 2328530, upload-time = "2025-05-02T11:16:49.938Z" }, + { url = "https://files.pythonhosted.org/packages/93/c3/9c035b9bad05156ad79399e96527ee09fb44734504077919459e83c39401/yara_python-4.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f8ca11d6877d453f69987b18963398744695841b4e2e56c2f1763002d5d22dbd", size = 2947508, upload-time = "2025-05-02T11:16:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/05/8b/d0e06cdc5767f64516864fb9d9a49a3956801cb1c76f757243dad7cb3891/yara_python-4.5.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f1f009d99e05f5be7c3d4e349c949226bfe32e0a9c3c75ff5476e94385824c26", size = 2418145, upload-time = "2025-05-02T11:16:53.248Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/8505c7486935b277622703ae7f3fb1ee83635d749facf0fed99aeb2078e3/yara_python-4.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96ead034a1aef94671ea92a82f1c2db6defa224cf21eb5139cff7e7345e55153", size = 2637499, upload-time = "2025-05-02T11:16:55.106Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3b/7bdf6a37b4df79f9c51599d5f02f96e8372bd0ddc53a3f2852016fda990f/yara_python-4.5.2-cp311-cp311-win32.whl", hash = "sha256:7b19ac28b3b55134ea12f1ee8500d7f695e735e9bead46b822abec96f9587f06", size = 1447050, upload-time = "2025-05-02T11:16:56.868Z" }, + { url = "https://files.pythonhosted.org/packages/78/1f/99fdeafb66702a3efe9072b7c8a8d4403938384eac7b4f46fc6e077ec484/yara_python-4.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:a699917ea1f3f47aecacd8a10b8ee82137205b69f9f29822f839a0ffda2c41a1", size = 1825468, upload-time = "2025-05-02T11:16:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1c/c91a0af659d7334e6899db3e9fc10deb0dae56232ac036bddc2f6ce14a7b/yara_python-4.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:037be5f9d5dd9f067bbbeeac5d311815611ba8298272a14b03d7ad0f42b36f5a", size = 403092, upload-time = "2025-05-02T11:16:59.62Z" }, + { url = "https://files.pythonhosted.org/packages/6c/87/146e9582e8f5b069d888ec410c02c4b3501ec024f2985b4903177d18dda1/yara_python-4.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:77c8192f56e2bbf42b0c16cd1c368ba7083047e5b11467c8b3d6330d268e1f86", size = 209528, upload-time = "2025-05-02T11:17:00.913Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/a41f6b62c4b7990dbc94582200bbd7fe52ee8e0aa2634c3733705811c93d/yara_python-4.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e892b2111122552f0645bc1a55f2525117470eea3b791d452de12ae0c1ec37b", size = 2244678, upload-time = "2025-05-02T11:17:02.332Z" }, + { url = "https://files.pythonhosted.org/packages/82/79/c3f96ff13349b0efd25b70f4ae423d37503116badf2af27df4e645f1f107/yara_python-4.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f16d9b23f107fd0569c676ec9340b24dd5a2a2a239a163dcdeaed6032933fb94", size = 2326396, upload-time = "2025-05-02T11:17:03.824Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/b55016776948b01fc3c989dd0257ee675ca1ce86011f502db7aa84cb9cd4/yara_python-4.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b98e0a77dc0f90bc53cf77cca1dc1a4e6836c7c5a283198c84d5dbb0701e722", size = 2331464, upload-time = "2025-05-02T11:17:05.674Z" }, + { url = "https://files.pythonhosted.org/packages/90/1d/4b9470380838b96681ed6b6254e7d236042b7871c0b662c4b8e9469eacf0/yara_python-4.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d6366d197079848d4c2534f07bc47f8a3c53d42855e6a492ed2191775e8cd294", size = 2949283, upload-time = "2025-05-02T11:17:07.191Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/65de40d1cd60386675d03a2a0c7679274e2be0fb76eaa878622a21497db8/yara_python-4.5.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a2ba9fddafe573614fc8e77973f07e74a359bd1f3a6152f93b814a6f8cfc0004", size = 2420959, upload-time = "2025-05-02T11:17:08.691Z" }, + { url = "https://files.pythonhosted.org/packages/6b/43/742d44b76c2483aebdf5b8c0495556416de7762c9becec18869f8830df01/yara_python-4.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3338f492e9bb655381dbf7e526201c1331d8c1e3760f1b06f382d901cc10cdf0", size = 2639613, upload-time = "2025-05-02T11:17:10.301Z" }, + { url = "https://files.pythonhosted.org/packages/ef/56/e7713f9e7afd51d3112b5c4c6a3d3a4f84ed7baf807967043a9ac7773f1b/yara_python-4.5.2-cp312-cp312-win32.whl", hash = "sha256:9d066da7f963f4a68a2681cbe1d7c41cb1ef2c5668de3a756731b1a7669a3120", size = 1447526, upload-time = "2025-05-02T11:17:12.002Z" }, + { url = "https://files.pythonhosted.org/packages/c1/90/65e96967cbfc65fd5d117580cb7601bc79d0c95f9dbd8b0ffbf25cdd0114/yara_python-4.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:fe5b4c9c5cb48526e8f9c67fc1fdafb9dbd9078a27d89af30de06424c8c67588", size = 1825673, upload-time = "2025-05-02T11:17:13.934Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/c55ff5d0abe89faffddff23c266b0247b93016eb97830bf079c873f9721c/yara_python-4.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ffc3101354188d23d00b831b0d070e2d1482a60d4e9964452004276f7c1edee8", size = 403087, upload-time = "2025-05-02T11:17:15.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/de/2d28216b23beca46d9eb3784c198cbf48068437b2220359f372d56a32dc0/yara_python-4.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c7021e6c4e34b2b11ad82de330728134831654ca1f5c24dcf093fedc0db07ae", size = 209503, upload-time = "2025-05-02T11:17:17.42Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/6a170b8eabffb3086b08f20ad9abfe7eb060c2ca06f6cf8b860155413575/yara_python-4.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73009bd6e73b04ffcbc8d47dddd4df87623207cb772492c516e16605ced5dd6", size = 2244791, upload-time = "2025-05-02T11:17:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b9/1b361bec1b7d1f216303ca8dbf85f417cf20dba0543272bb7897e7853ce7/yara_python-4.5.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef7f592db5e2330efd01b40760c3e2be5de497ff22bd6d12e63e9cf6f37b4213", size = 2326306, upload-time = "2025-05-02T11:17:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b7/ca274eee26237ae60da2bca66288737d7b503aa67032b756059cdb69d254/yara_python-4.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5980d96ac2742e997e55ba20d2746d3a42298bbb5e7d327562a01bac70c9268", size = 2331285, upload-time = "2025-05-02T11:17:21.684Z" }, + { url = "https://files.pythonhosted.org/packages/40/8b/32acec8da17f91377331d55d01c90f6cd3971584209ae29647a6ce29721d/yara_python-4.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e857bc94ef54d5db89e0a58652df609753d5b95f837dde101e1058dd755896b5", size = 2949045, upload-time = "2025-05-02T11:17:23.206Z" }, + { url = "https://files.pythonhosted.org/packages/76/42/4d1f67f09b10e0aa087214522fb1ea7fe68b54bb1d09111687d3683956f6/yara_python-4.5.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98b4732a9f5b184ade78b4675501fbdc4975302dc78aa3e917c60ca4553980d5", size = 2420755, upload-time = "2025-05-02T11:17:25.178Z" }, + { url = "https://files.pythonhosted.org/packages/76/c1/ea7a67235e9c43a27c89454c38555338683015a3cc9fe20f44a2163f2361/yara_python-4.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57928557c85af27d27cca21de66d2070bf1860de365fb18fc591ddfb1778b959", size = 2639331, upload-time = "2025-05-02T11:17:26.697Z" }, + { url = "https://files.pythonhosted.org/packages/42/82/64ef5a30f39554f7b58e8a42db31dd284766d50504fb23d19475becc83f8/yara_python-4.5.2-cp313-cp313-win32.whl", hash = "sha256:d7b58296ed2d262468d58f213b19df3738e48d46b8577485aecca0edf703169f", size = 1447528, upload-time = "2025-05-02T11:17:28.166Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/93452ff294669f935d457ec5376a599dd93da0ac7a9a590e58c1e537df13/yara_python-4.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f6ccde3f30d0c3cda9a86e91f2a74073c9aeb127856d9a62ed5c4bb22ccd75f", size = 1825770, upload-time = "2025-05-02T11:17:29.584Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]