Repository: byt3bl33d3r/dnschef-ng
Branch: main
Commit: e8f3b4904cf5
Files: 34
Total size: 154.0 KB
Directory structure:
gitextract_1kroxokl/
├── .devcontainer/
│ └── devcontainer.json
├── .dockerignore
├── .github/
│ └── workflows/
│ ├── python-package.yml
│ ├── python-publish-test.yml
│ └── python-publish.yml
├── .gitignore
├── CHANGELOG
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── TODO
├── dnschef/
│ ├── __init__.py
│ ├── __main__.py
│ ├── api.py
│ ├── kitchen.py
│ ├── logger.py
│ ├── protocols.py
│ └── utils.py
├── dnschef.toml
├── docker-compose.yml
├── pyproject.toml
├── requirements-api.txt
├── requirements-dev.txt
├── requirements.txt
└── tests/
├── __init__.py
├── conftest.py
├── dnschef-tests.toml
├── small-bin-test
├── test_dns_server.py
├── test_file_staging.py
├── test_http_api.py
├── test_util.py
└── thicc-bin-test
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "DNSChef-NG",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:1-3.11",
"features": {
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
},
"customizations": {
"vscode": {
"extensions": [
"tamasfe.even-better-toml"
]
}
}
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip3 install --user -r requirements.txt",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
================================================
FILE: .dockerignore
================================================
tests
__pycache__
*.pyc
================================================
FILE: .github/workflows/python-package.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python3 -m pip install --user pipx
pipx install poetry
poetry install --all-extras
- name: Lint with ruff
run: |
# stop the build if there are Python syntax errors or undefined names
poetry run ruff --select=E9,F63,F7,F82 --exit-zero --statistics .
poetry run ruff --select=E9,F63,F7,F82 --show-source .
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
poetry run ruff --exit-zero --statistics .
- name: Test with pytest
run: |
poetry run pytest
================================================
FILE: .github/workflows/python-publish-test.yml
================================================
name: Upload Package to PyPi Testing
on:
workflow_dispatch:
#release:
# types: [published]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pipx
pipx install poetry
- name: Build and publish package
run: |
poetry config pypi-token.testpypi ${{ secrets.PYPI_TESTING_API_TOKEN }}
poetry config repositories.testpypi https://test.pypi.org/legacy/
poetry publish -r testpypi --build
================================================
FILE: .github/workflows/python-publish.yml
================================================
name: Upload Package to PyPi
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pipx
pipx install poetry
- name: Build and publish package
run: |
poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}
poetry publish --build
================================================
FILE: .gitignore
================================================
.vscode
.DS_Store
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.log
.ruff_cache
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
================================================
FILE: CHANGELOG
================================================
Version 0.5
* Complete re-write, now fully asynchronous (uses Python's AsyncIO library)
Version 0.4
* Ported to Python 3.6+
* Made everything a bit more PEP8 compliant
* Improved logging
* Removed IPy library (replaced with built-in ipaddress library)
Version 0.3
* Added support for the latest version of the dnslib library - 0.9.3
* Added support for logging. (idea by kafeine)
* Added support for SRV, DNSKEY, and RRSIG records. (idea by mubix)
* Added support for TCP remote nameserver connections. (idea by mubix)
* DNS name matching is now case insensitive.
* Various small bug fixes and performance tweaks.
* Python libraries are no longer bundled with the distribution, but
compiled in the Windows binary.
Version 0.2.1
* Fixed a Python 2.6 compatibility issue. (thanks Mehran Goudarzi)
Version 0.2
* Added IPv6 support.
* Added AAAA, MX, CNAME, NS, SOA and NAPTR support.
* Added support for ANY queries (returns all known fake records).
* Changed file format to support more DNS record types.
* Added alternative DNS port support (contributed by fnv).
* Added alternative listening port support for the server (contributed by Mark Straver).
* Updated bundled dnslib library to the latest version - 0.8.2.
* Included IPy library for IPv6 support.
Version 0.1
* First public release
================================================
FILE: Dockerfile
================================================
FROM python:3.11-slim as build-stage
WORKDIR /tmp/code
COPY . .
RUN pip wheel --wheel-dir ./dist '.[api]'
FROM python:3.11-slim
WORKDIR /app
COPY --from=build-stage /tmp/code/dist/ .
RUN pip install --no-cache-dir --no-index --find-links . dnschef[api]
EXPOSE 80 53/udp 53/tcp
CMD ["uvicorn", "dnschef.api:app", "--host", "0.0.0.0", "--port", "80"]
# If using a proxy
#CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
================================================
FILE: LICENSE
================================================
Copyright (C) 2014 Peter Kacherginsky, Marcello Salvati
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: Makefile
================================================
.PHONY: tests
default: build
clean:
rm -f -r build/
rm -f -r bin/
rm -f -r dist/
rm -f -r *.egg-info
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -rf {} +
find . -name '.pytest_cache' -exec rm -rf {} +
tests:
ruff --select=E9,F63,F7,F82 --show-source .
python -m pytest
requirements:
poetry export -f requirements.txt > requirements.txt
poetry export -f requirements.txt --extras=api > requirements-api.txt
poetry export -f requirements.txt --with=dev > requirements-dev.txt
================================================
FILE: README.md
================================================
> [!NOTE]
> This is an updated version of [DNSChef](https://github.com/iphelix/dnschef) originally written by [@iphelix](https://github.com/iphelix)
```
_ _ __
| | v0.7 | | / _|
__| |_ __ ___ ___| |__ ___| |_ ______ _ __ __ _
/ _` | '_ \/ __|/ __| '_ \ / _ \ _|______| '_ \ / _` |
| (_| | | | \__ \ (__| | | | __/ | | | | | (_| |
\__,_|_| |_|___/\___|_| |_|\___|_| |_| |_|\__, |
__/ |
|___/
D O C U M E N T A T I O N
```
DNSChef is a highly configurable DNS proxy for Penetration Testers and Malware Analysts. A DNS proxy (aka "Fake DNS") is a tool used for application network traffic analysis among other uses. For example, a DNS proxy can be used to fake requests for "badguy.com" to point to a local machine for termination or interception instead of a real host somewhere on the Internet.
There are several DNS Proxies out there. Most will simply point all DNS queries a single IP address or implement only rudimentary filtering. DNSChef was developed as part of a penetration test where there was a need for a more configurable system. As a result, DNSChef is cross-platform application capable of forging responses based on inclusive and exclusive domain lists, supporting multiple DNS record types, matching domains with wildcards, proxying true responses for nonmatching domains, defining external configuration files, IPv6 and many other features. You can find detailed explanation of each of the features and suggested uses below.
The use of DNS Proxy is recommended in situations where it is not possible to force an application to use some other proxy server directly. For example, some mobile applications completely ignore OS HTTP Proxy settings. In these cases, the use of a DNS proxy server such as DNSChef will allow you to trick that application into forwarding connections to the desired destination.
## New Features
- Requires Python 3.11+
- Supports staging files over DNS (only over `A`,`AAAA`,`TXT` for now...)
- Config file is now TOML
- Optional HTTP API (allows you to query logs and update config remotely)
- Fully async for increased performance (uses AsyncIO)
- Structured logging and a number of QOL improvements
- Is now a Python package
- Dockerized
- Includes a number of the PRs and fixes from the original repo
## Installing
To install the latest release you should use [pipx](https://pypa.github.io/pipx/) (unless you're a piece of shit who enjoys sloppy stakes):
pipx install dnschef-ng
If you want the HTTP API (requires some extra dependencies):
pipx install dnschef-ng[api]
Install latest version from Git using pipx:
pipx install git+https://github.com/byt3bl33d3r/dnschef-ng.git
Install latest version from Git using pipx with the deps for the HTTP API:
pipx install "git+https://github.com/byt3bl33d3r/dnschef-ng.git#egg=dnschef-ng[api]"
## Setting up a DNS Proxy
Before you can start using DNSChef, you must configure your machine to use a DNS nameserver with the tool running on it. You have several options based on the operating system you are going to use:
- **Linux** - Edit */etc/resolv.conf* to include a line on the very top with your traffic analysis host (e.g add "nameserver 127.0.0.1" if you are running locally). Alternatively, you can add a DNS server address using tools such as Network Manager. Inside the Network Manager open IPv4 Settings, select *Automatic (DHCP) addresses only* or *Manual* from the *Method* drop down box and edit *DNS Servers* text box to include an IP address with DNSChef running.
- **Windows** - Select *Network Connections* from the *Control Panel*. Next select one of the connections (e.g. "Local Area Connection"), right-click on it and select properties. From within a newly appearing dialog box, select *Internet Protocol (TCP/IP)* and click on properties. At last select *Use the following DNS server addresses* radio button and enter the IP address with DNSChef running. For example, if running locally enter 127.0.0.1.
- **OS X** - Open *System Preferences* and click on the *Network* icon. Select the active interface and fill in the *DNS Server* field. If you are using Airport then you will have to click on *Advanced...* button and edit DNS servers from there. Alternatively, you can edit */etc/resolv.conf* and add a fake nameserver to the very top there (e.g "nameserver 127.0.0.1").
- **iOS** - Open *Settings* and select *General*. Next select on *Wi-Fi* and click on a blue arrow to the right of an active Access Point from the list. Edit DNS entry to point to the host with DNSChef running. Make sure you have disabled Cellular interface (if available).
- **Android** - Open *Settings* and select *Wireless and network*. Click on *Wi-Fi settings* and select *Advanced* after pressing the *Options* button on the phone. Enable *Use static IP* checkbox and configure a custom DNS server.
If you do not have the ability to modify device's DNS settings manually, then you still have several options involving techniques such as [ARP Spoofing](http://en.wikipedia.org/wiki/ARP_spoofing), [Rogue DHCP](http://www.yersinia.net/doc.htm) and other creative methods.
At last you need to configure a fake service where DNSChef will point all of the requests. For example, if you are trying to intercept web traffic, you must bring up either a separate web server running on port 80 or set up a web proxy (e.g. Burp) to intercept traffic. DNSChef will point queries to your proxy/server host with properly configured services.
## Running DNSChef
DNSChef is a cross-platform application developed in Python which should run on most platforms which have a Python interpreter. This guide will concentrate on Unix environments; however, all of the examples below were tested to work on Windows as well.
Let's get a taste of DNSChef with its most basic monitoring functionality. Execute the following command as root (required to start a server on port 53):
# ./dnschef.py
_ _ __
| | version 0.2 | | / _|
__| |_ __ ___ ___| |__ ___| |_
/ _` | '_ \/ __|/ __| '_ \ / _ \ _|
| (_| | | | \__ \ (__| | | | __/ |
\__,_|_| |_|___/\___|_| |_|\___|_|
iphelix@thesprawl.org
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[*] No parameters were specified. Running in full proxy mode
Without any parameters, DNSChef will run in full proxy mode. This means that all requests will simply be forwarded to an upstream DNS server (8.8.8.8 by default) and returned back to the quering host. For example, let's query an "A" record for a domain and observe results:
$ host -t A thesprawl.org
thesprawl.org has address 108.59.3.64
DNSChef will print the following log line showing time, source IP address, type of record requested and most importantly which name was queried:
[23:54:03] 127.0.0.1: proxying the response of type 'A' for thesprawl.org
This mode is useful for simple application monitoring where you need to figure out which domains it uses for its communications.
DNSChef has full support for IPv6 which can be activated using *-6* or *--ipv6** flags. It works exactly as IPv4 mode with the exception that default listening interface is switched to ::1 and default DNS server is switched to 2001:4860:4860::8888. Here is a sample output:
# ./dnschef.py -6
_ _ __
| | version 0.2 | | / _|
__| |_ __ ___ ___| |__ ___| |_
/ _` | '_ \/ __|/ __| '_ \ / _ \ _|
| (_| | | | \__ \ (__| | | | __/ |
\__,_|_| |_|___/\___|_| |_|\___|_|
iphelix@thesprawl.org
[*] Using IPv6 mode.
[*] DNSChef started on interface: ::1
[*] Using the following nameservers: 2001:4860:4860::8888
[*] No parameters were specified. Running in full proxy mode
[00:35:44] ::1: proxying the response of type 'A' for thesprawl.org
[00:35:44] ::1: proxying the response of type 'AAAA' for thesprawl.org
[00:35:44] ::1: proxying the response of type 'MX' for thesprawl.org
NOTE: By default, DNSChef creates a UDP listener. You can use TCP instead with the *--tcp* argument discussed later.
## Running the DNSChef HTTP API
> [!WARNING]
> The API has no authentication. Allow/deny access at the network level through security groups, iptables, firewall etc..
`uvicorn dnschef.api:app`
You can then view the OpenAPI documentation at `http://127.0.0.1:8000/docs`
```
$ uvicorn dnschef.api:app
INFO: Started server process [28327]
INFO: Waiting for application startup.
_ _ __
| | version 0.6.0 | | / _|
__| |_ __ ___ ___| |__ ___| |_
/ _` | '_ \/ __|/ __| '_ \ / _ \ _|
| (_| | | | \__ \ (__| | | | __/ |
\__,_|_| |_|___/\___|_| |_|\___|_|
@iphelix // @byt3bl33d3r
2023-09-28 11:24:59 cooking replies domain=*.thesprawl.org record=192.0.2.1 section=A
2023-09-28 11:24:59 cooking replies domain=*.thesprawl.org record=2001:db8::1 section=AAAA
-- SNIP --
2023-09-28 11:24:59 cooking replies domain=*.thesprawl.org record=1 . alpn=h2 ipv4hint=127.0.0.1 ipv6hint=::1 section=HTTPS
INFO: Application startup complete.
2023-09-28 11:24:59 DNSChef is active interface=127.0.0.1 ipv6=False nameservers=['8.8.8.8'] port=53 tcp=False
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
## Intercept all responses
Now, that you know how to start DNSChef let's configure it to fake all replies to point to 127.0.0.1 using the *--fakeip* parameter:
# ./dnschef.py --fakeip 127.0.0.1 -q
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[*] Cooking all A replies to point to 127.0.0.1
[23:55:57] 127.0.0.1: cooking the response of type 'A' for google.com to 127.0.0.1
[23:55:57] 127.0.0.1: proxying the response of type 'AAAA' for google.com
[23:55:57] 127.0.0.1: proxying the response of type 'MX' for google.com
In the above output you an see that DNSChef was configured to proxy all requests to 127.0.0.1. The first line of log at 08:11:23 shows that we have "cooked" the "A" record response to point to 127.0.0.1. However, further requests for 'AAAA' and 'MX' records are simply proxied from a real DNS server. Let's see the output from requesting program:
$ host google.com localhost
google.com has address 127.0.0.1
google.com has IPv6 address 2001:4860:4001:803::1001
google.com mail is handled by 10 aspmx.l.google.com.
google.com mail is handled by 40 alt3.aspmx.l.google.com.
google.com mail is handled by 30 alt2.aspmx.l.google.com.
google.com mail is handled by 20 alt1.aspmx.l.google.com.
google.com mail is handled by 50 alt4.aspmx.l.google.com.
As you can see the program was tricked to use 127.0.0.1 for the IPv4 address. However, the information obtained from IPv6 (AAAA) and mail (MX) records appears completely legitimate. The goal of DNSChef is to have the least impact on the correct operation of the program, so if an application relies on a specific mailserver it will correctly obtain one through this proxied request.
Let's fake one more request to illustrate how to target multiple records at the same time:
# ./dnschef.py --fakeip 127.0.0.1 --fakeipv6 ::1 -q
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[*] Cooking all A replies to point to 127.0.0.1
[*] Cooking all AAAA replies to point to ::1
[00:02:14] 127.0.0.1: cooking the response of type 'A' for google.com to 127.0.0.1
[00:02:14] 127.0.0.1: cooking the response of type 'AAAA' for google.com to ::1
[00:02:14] 127.0.0.1: proxying the response of type 'MX' for google.com
In addition to the --fakeip flag, I have now specified --fakeipv6 designed to fake 'AAAA' record queries. Here is an updated program output:
$ host google.com localhost
google.com has address 127.0.0.1
google.com has IPv6 address ::1
google.com mail is handled by 10 aspmx.l.google.com.
google.com mail is handled by 40 alt3.aspmx.l.google.com.
google.com mail is handled by 30 alt2.aspmx.l.google.com.
google.com mail is handled by 20 alt1.aspmx.l.google.com.
google.com mail is handled by 50 alt4.aspmx.l.google.com.
Once more all of the records not explicitly overriden by the application were proxied and returned from the real DNS server. However, IPv4 (A) and IPv6 (AAAA) were both faked to point to a local machine.
DNSChef supports multiple record types:
Record | Description | Argument | Example
---|---|---|---
A | IPv4 address |--fakeip | --fakeip 192.0.2.1
AAAA | IPv6 address |--fakeipv6 | --fakeipv6 2001:db8::1
MX | Mail server |--fakemail | --fakemail mail.fake.com
CNAME | CNAME record |--fakealias| --fakealias www.fake.com
NS | Name server |--fakens | --fakens ns.fake.com
NOTE: For usability not all DNS record types are exposed on the command line. Additional records such as PTR, TXT, SOA, etc. can be specified using the --file flag and an appropriate record header. See the [external definitions file](#external-definitions-file) section below for details.
At last let's observe how the application handles queries of type ANY:
# ./dnschef.py --fakeip 127.0.0.1 --fakeipv6 ::1 --fakemail mail.fake.com --fakealias www.fake.com --fakens ns.fake.com -q
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[*] Cooking all A replies to point to 127.0.0.1
[*] Cooking all AAAA replies to point to ::1
[*] Cooking all MX replies to point to mail.fake.com
[*] Cooking all CNAME replies to point to www.fake.com
[*] Cooking all NS replies to point to ns.fake.com
[00:17:29] 127.0.0.1: cooking the response of type 'ANY' for google.com with all known fake records.
DNS ANY record queries results in DNSChef returning every faked record that it knows about for an applicable domain. Here is the output that the program will see:
# host -t ANY google.com localhost
google.com has address 127.0.0.1
google.com has IPv6 address ::1
google.com mail is handled by 10 mail.fake.com.
google.com is an alias for www.fake.com.
google.com name server ns.fake.com.
## Filtering domains
Using the above example, consider you only want to intercept requests for *thesprawl.org* and leave queries to all other domains such as *webfaction.com* without modification. You can use the *--fakedomains* parameter as illustrated below:
# ./dnschef.py --fakeip 127.0.0.1 --fakedomains thesprawl.org -q
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[*] Cooking replies to point to 127.0.0.1 matching: thesprawl.org
[00:23:37] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 127.0.0.1
[00:23:52] 127.0.0.1: proxying the response of type 'A' for mx9.webfaction.com
From the above example the request for *thesprawl.org* was faked; however, the request for *mx9.webfaction.com* was left alone. Filtering domains is very useful when you attempt to isolate a single application without breaking the rest.
**NOTE**: DNSChef will not verify whether the domain exists or not before faking the response. If you have specified a domain it will always resolve to a fake value whether it really exists or not.
## Reverse filtering
In another situation you may need to fake responses for all requests except a defined list of domains. You can accomplish this task using the *--truedomains* parameter as follows:
# ./dnschef.py --fakeip 127.0.0.1 --truedomains thesprawl.org,*.webfaction.com -q
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[*] Cooking replies to point to 127.0.0.1 not matching: *.webfaction.com, thesprawl.org
[00:27:57] 127.0.0.1: proxying the response of type 'A' for mx9.webfaction.com
[00:28:05] 127.0.0.1: cooking the response of type 'A' for google.com to 127.0.0.1
There are several things going on in the above example. First notice the use of a wildcard (*). All domains matching *.webfaction.com will be reverse matched and resolved to their true values. The request for 'google.com' returned 127.0.0.1 because it was not on the list of excluded domains.
**NOTE**: Wildcards are position specific. A mask of type *.thesprawl.org will match www.thesprawl.org but not www.test.thesprawl.org. However, a mask of type *.*.thesprawl.org will match thesprawl.org, www.thesprawl.org and www.test.thesprawl.org.
## External definitions file
There may be situations where defining a single fake DNS record for all matching domains may not be sufficient. You can use an external file with a collection of DOMAIN=RECORD pairs defining exactly where you want the request to go.
For example, let create the following definitions file and call it `dnschef.toml`:
```toml
[A]
"*.google.com"="192.0.2.1"
"thesprawl.org"="192.0.2.2"
"*.wordpress.*"="192.0.2.3"
```
Notice the section header `[A]`, it defines the record type to DNSChef. Now let's carefully observe the output of multiple queries:
# ./dnschef.py --file dnschef.toml -q
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[+] Cooking A replies for domain *.google.com with '192.0.2.1'
[+] Cooking A replies for domain thesprawl.org with '192.0.2.2'
[+] Cooking A replies for domain *.wordpress.* with '192.0.2.3'
[00:43:54] 127.0.0.1: cooking the response of type 'A' for google.com to 192.0.2.1
[00:44:05] 127.0.0.1: cooking the response of type 'A' for www.google.com to 192.0.2.1
[00:44:19] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 192.0.2.2
[00:44:29] 127.0.0.1: proxying the response of type 'A' for www.thesprawl.org
[00:44:40] 127.0.0.1: cooking the response of type 'A' for www.wordpress.org to 192.0.2.3
[00:44:51] 127.0.0.1: cooking the response of type 'A' for wordpress.com to 192.0.2.3
[00:45:02] 127.0.0.1: proxying the response of type 'A' for slashdot.org
Both *google.com* and *www.google.com* matched the *\*.google.com* entry and correctly resolved to *192.0.2.1*. On the other hand *www.thesprawl.org* request was simply proxied instead of being modified. At last all variations of *wordpress.com*, *www.wordpress.org*, etc. matched the *\*.wordpress.\** mask and correctly resolved to *192.0.2.3*. At last an undefined *slashdot.org* query was simply proxied with a real response.
You can specify section headers for all other supported DNS record types including the ones not explicitly exposed on the command line: [A], [AAAA], [MX], [NS], [CNAME], [PTR], [NAPTR] and [SOA]. For example, let's define a new [PTR] section in the `dnschef.toml` file:
```toml
[PTR]
"*.2.0.192.in-addr.arpa"="fake.com"
```
Let's observe DNSChef's behavior with this new record type:
./dnschef.py --file dnschef.toml -q
[sudo] password for iphelix:
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[+] Cooking PTR replies for domain *.2.0.192.in-addr.arpa with 'fake.com'
[00:11:34] 127.0.0.1: cooking the response of type 'PTR' for 1.2.0.192.in-addr.arpa to fake.com
And here is what a client might see when performing reverse DNS queries:
$ host 192.0.2.1 localhost
1.2.0.192.in-addr.arpa domain name pointer fake.com.
Some records require exact formatting. Good examples are SOA and NAPTR
```toml
[SOA]
"*.thesprawl.org" = "ns.fake.com. hostmaster.fake.com. 1 10800 3600 604800 3600"
[NAPTR]
"*.thesprawl.org" = "100 10 U E2U+sip !^.*$!sip:customer-service@fake.com! ."
```
See sample `dnschef.toml` file for additional examples.
## File Staging
DNSChef can "stage" any file through DNS. Currently file staging is only supported with `A`, `AAAA` and `TXT` records (will be adding more). To instruct DNSChef to stage a file, add the following section to your `dnschef.toml`:
```toml
[A]
"*.wat.org" = { file = "/home/payload.exe", chunk_size = 4 }
[AAAA]
"*.gorgetowngeronimos.org" = { file = "/home/payload.exe", chunk_size = 16 }
```
> [!NOTE]
> The `chunk_size` setting is optional and it's behavior is highly dependent on the query type. Example: As `A` queries return an IPv4 address, the maximum allowed `chunk_size` is 4 bytes. Setting the `chunk_size` to anything above 4 will be ignored.
An `A` query to `*.wat.org` containing a number in the DNS name will now return the corresponding chunk of the file. E.g the query `ns0.wat.org` will return an IPv4 address containing the first chunk of the file (4 bytes). A query for `test1.wat.org` will return the second chunk of the file etc...
When using wildcard domains like the above examples, the "chunk" numbers can be placed anywhere and don't have to be put together. E.g an `A` query for `1aliens2.wat.org` will return the 12th chunk of the file.
`TXT` records support additional options for file staging as they allow more flexibility:
```toml
[TXT]
"ns*.dungbeetle.org" = { file = "~/payload.exe", chunk_size = 189, response_format = "{prefix}test-{chunk}", response_prefix_pool = ["atlassian-domain-verification=", "onetrust-domain-verification=", "docusign=" ] }
```
With this configuration, any `TXT` query to `ns*.dungbeetle.org` will return a chunk of our file located locally on the filesystem at `~/payload.exe`.
The `response_format` and `response_prefix_pool` settings are optional but allow you to further customize the DNS `TXT` response.
The `response_format` setting defines the format of the `TXT` response:
- The `{prefix}` variable will be randomly substituted with one of the values defined in the `response_prefix_pool` array.
- The `{chunk}` variable will be replaced with the file chunk.
With the above configuration, a `TXT` query to `ns1.dungbeetle.org` will return the following response:
```
docusign=test-<BASE64_ENCODED_FILE_CHUNK_N1>
```
If you perform another `TXT` query (e.g. `ns10.dungbeetle.org`), you'll see that the prefix will change:
```
atlassian-domain-verification=test-<BASE64_ENCODED_FILE_CHUNK_N10>
```
## Advanced Filtering
You can mix and match input from a file and command line. For example the following command uses both `--file` and `--fakedomains` parameters:
# ./dnschef.py --file dnschef.toml --fakeip 6.6.6.6 --fakedomains=thesprawl.org,slashdot.org -q
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[+] Cooking A replies for domain *.google.com with '192.0.2.1'
[+] Cooking A replies for domain thesprawl.org with '192.0.2.2'
[+] Cooking A replies for domain *.wordpress.* with '192.0.2.3'
[*] Cooking A replies to point to 6.6.6.6 matching: *.wordpress.*, *.google.com, thesprawl.org
[*] Cooking A replies to point to 6.6.6.6 matching: slashdot.org, *.wordpress.*, *.google.com, thesprawl.org
[00:49:05] 127.0.0.1: cooking the response of type 'A' for google.com to 192.0.2.1
[00:49:15] 127.0.0.1: cooking the response of type 'A' for slashdot.org to 6.6.6.6
[00:49:31] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 6.6.6.6
[00:50:08] 127.0.0.1: proxying the response of type 'A' for tor.com
Notice the definition for *thesprawl.org* in the command line parameter took precedence over *dnschef.toml*. This could be useful if you want to override values in the configuration file. slashdot.org still resolves to the fake IP address because it was specified in the *--fakedomains* parameter. tor.com request is simply proxied since it was not specified in either command line or the configuration file.
## Other configurations
For security reasons, DNSChef listens on a local 127.0.0.1 (or ::1 for IPv6) interface by default. You can make DNSChef listen on another interface using the *--interface* parameter:
# ./dnschef.py --interface 0.0.0.0 -q
[*] DNSChef started on interface: 0.0.0.0
[*] Using the following nameservers: 8.8.8.8
[*] No parameters were specified. Running in full proxy mode
[00:50:53] 192.0.2.105: proxying the response of type 'A' for thesprawl.org
or for IPv6:
# ./dnschef.py -6 --interface :: -q
[*] Using IPv6 mode.
[*] DNSChef started on interface: ::
[*] Using the following nameservers: 2001:4860:4860::8888
[*] No parameters were specified. Running in full proxy mode
[00:57:46] 2001:db8::105: proxying the response of type 'A' for thesprawl.org
By default, DNSChef uses Google's public DNS server to make proxy requests. However, you can define a custom list of nameservers using the *--nameservers* parameter:
# ./dnschef.py --nameservers 4.2.2.1,4.2.2.2 -q
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 4.2.2.1, 4.2.2.2
[*] No parameters were specified. Running in full proxy mode
[00:55:08] 127.0.0.1: proxying the response of type 'A' for thesprawl.org
It is possible to specify non-standard nameserver port using IP#PORT notation:
# ./dnschef.py --nameservers 192.0.2.2#5353 -q
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 192.0.2.2#5353
[*] No parameters were specified. Running in full proxy mode
[02:03:12] 127.0.0.1: proxying the response of type 'A' for thesprawl.org
At the same time it is possible to start DNSChef itself on an alternative port using the `-p port#` parameter:
# ./dnschef.py -p 5353 -q
[*] Listening on an alternative port 5353
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[*] No parameters were specified. Running in full proxy mode
DNS protocol can be used over UDP (default) or TCP. DNSChef implements a TCP mode which can be activated with the `--tcp` flag.
================================================
FILE: TODO
================================================
[*] Run in MiTM mode and inject fake DNS responses.
================================================
FILE: dnschef/__init__.py
================================================
import importlib.metadata
__version__ = importlib.metadata.version("dnschef-ng")
================================================
FILE: dnschef/__main__.py
================================================
#!/usr/bin/env python3
#
# DNSChef is a highly configurable DNS Proxy for Penetration Testers
# and Malware Analysts. Please visit http://thesprawl.org/projects/dnschef/
# for the latest version and documentation. Please forward all issues and
# concerns to iphelix [at] thesprawl.org.
# Copyright (C) 2019 Peter Kacherginsky, Marcello Salvati
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from dnschef import kitchen
from dnschef.protocols import start_server
from dnschef.logger import log, plain_formatter, debug_formatter
from dnschef.utils import header, parse_config_file
from argparse import ArgumentParser
import asyncio
import logging
import logging.handlers
import sys
def main():
# Parse command line arguments
parser = ArgumentParser(usage = "dnschef.py [options]:\n" + header, description="DNSChef is a highly configurable DNS Proxy for Penetration Testers and Malware Analysts. It is capable of fine configuration of which DNS replies to modify or to simply proxy with real responses. In order to take advantage of the tool you must either manually configure or poison DNS server entry to point to DNSChef. The tool requires root privileges to run on privileged ports." )
fakegroup = parser.add_argument_group("Fake DNS records:")
fakegroup.add_argument('--fakeip', metavar="192.0.2.1", help='IP address to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'A\' queries will be spoofed. Consider using --file argument if you need to define more than one IP address.')
fakegroup.add_argument('--fakeipv6', metavar="2001:db8::1", help='IPv6 address to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'AAAA\' queries will be spoofed. Consider using --file argument if you need to define more than one IPv6 address.')
fakegroup.add_argument('--fakemail', metavar="mail.fake.com", help='MX name to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'MX\' queries will be spoofed. Consider using --file argument if you need to define more than one MX record.')
fakegroup.add_argument('--fakealias', metavar="www.fake.com", help='CNAME name to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'CNAME\' queries will be spoofed. Consider using --file argument if you need to define more than one CNAME record.')
fakegroup.add_argument('--fakens', metavar="ns.fake.com", help='NS name to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'NS\' queries will be spoofed. Consider using --file argument if you need to define more than one NS record.')
fakegroup.add_argument('--file', help="Specify a file containing a list of DOMAIN=IP pairs (one pair per line) used for DNS responses. For example: google.com=1.1.1.1 will force all queries to 'google.com' to be resolved to '1.1.1.1'. IPv6 addresses will be automatically detected. You can be even more specific by combining --file with other arguments. However, data obtained from the file will take precedence over others.")
mexclusivegroup = parser.add_mutually_exclusive_group()
mexclusivegroup.add_argument('--fakedomains', metavar="thesprawl.org,google.com", help='A comma separated list of domain names which will be resolved to FAKE values specified in the the above parameters. All other domain names will be resolved to their true values.')
mexclusivegroup.add_argument('--truedomains', metavar="thesprawl.org,google.com", help='A comma separated list of domain names which will be resolved to their TRUE values. All other domain names will be resolved to fake values specified in the above parameters.')
rungroup = parser.add_argument_group("Optional runtime parameters.")
rungroup.add_argument("--logfile", metavar="FILE", help="Specify a log file to record all activity")
rungroup.add_argument("--nameservers", metavar="8.8.8.8#53 or 4.2.2.1#53#tcp or 2001:4860:4860::8888", default='8.8.8.8', help='A comma separated list of alternative DNS servers to use with proxied requests. Nameservers can have either IP or IP#PORT format. A randomly selected server from the list will be used for proxy requests when provided with multiple servers. By default, the tool uses Google\'s public DNS server 8.8.8.8 when running in IPv4 mode and 2001:4860:4860::8888 when running in IPv6 mode.')
rungroup.add_argument("-i","--interface", metavar="127.0.0.1 or ::1", default="127.0.0.1", help='Define an interface to use for the DNS listener. By default, the tool uses 127.0.0.1 for IPv4 mode and ::1 for IPv6 mode.')
rungroup.add_argument("-t","--tcp", action="store_true", default=False, help="Use TCP DNS proxy instead of the default UDP.")
rungroup.add_argument("-6","--ipv6", action="store_true", default=False, help="Run in IPv6 mode.")
rungroup.add_argument("-p","--port", metavar=53, default=53, type=int, help='Port number to listen for DNS requests.')
rungroup.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Run in verbose mode")
options = parser.parse_args()
# Print program header
print(header)
if options.verbose:
log.setLevel(logging.DEBUG)
log.handlers[0].setFormatter(debug_formatter)
log.debug("running in verbose mode")
if not (options.fakeip or options.fakeipv6) and (options.fakedomains or options.truedomains):
log.error("you have forgotten to specify which IP to use for fake responses")
sys.exit(0)
# Adjust defaults for IPv6
if options.ipv6:
if options.interface == "127.0.0.1":
options.interface = "::1"
if options.nameservers == "8.8.8.8":
options.nameservers = "2001:4860:4860::8888"
# Use alternative DNS servers
if options.nameservers:
nameservers = options.nameservers.split(',')
# External file definitions
if options.file:
kitchen.CONFIG = parse_config_file(options.file)
# DNS Record and Domain Name definitions
if options.fakeip or options.fakeipv6 or options.fakemail or options.fakealias or options.fakens:
fakeip = options.fakeip
fakeipv6 = options.fakeipv6
fakemail = options.fakemail
fakealias = options.fakealias
fakens = options.fakens
if options.fakedomains:
for domain in options.fakedomains.split(','):
# Make domain case insensitive
domain = domain.lower()
domain = domain.strip()
if fakeip:
kitchen.CONFIG["A"][domain] = fakeip
log.info(f"cooking A replies to point to {options.fakeip} matching: {domain}")
if fakeipv6:
kitchen.CONFIG["AAAA"][domain] = fakeipv6
log.info(f"cooking AAAA replies to point to {options.fakeipv6} matching: {domain}")
if fakemail:
kitchen.CONFIG["MX"][domain] = fakemail
log.info(f"cooking MX replies to point to {options.fakemail} matching: {domain}")
if fakealias:
kitchen.CONFIG["CNAME"][domain] = fakealias
log.info(f"cooking CNAME replies to point to {options.fakealias} matching: {domain}")
if fakens:
kitchen.CONFIG["NS"][domain] = fakens
log.info(f"cooking NS replies to point to {options.fakens} matching: {domain}")
elif options.truedomains:
for domain in options.truedomains.split(','):
# Make domain case insensitive
domain = domain.lower()
domain = domain.strip()
if fakeip:
kitchen.CONFIG["A"][domain] = False
log.info(f"cooking A replies to point to {options.fakeip} not matching: {domain}")
kitchen.CONFIG["A"]['*'] = fakeip
if fakeipv6:
kitchen.CONFIG["AAAA"][domain] = False
log.info(f"cooking AAAA replies to point to {options.fakeipv6} not matching: {domain}")
kitchen.CONFIG["AAAA"]['*'] = fakeipv6
if fakemail:
kitchen.CONFIG["MX"][domain] = False
log.info(f"cooking MX replies to point to {options.fakemail} not matching: {domain}")
kitchen.CONFIG["MX"]['*'] = fakemail
if fakealias:
kitchen.CONFIG["CNAME"][domain] = False
log.info(f"cooking CNAME replies to point to {options.fakealias} not matching: {domain}")
kitchen.CONFIG["CNAME"]['*'] = fakealias
if fakens:
kitchen.CONFIG["NS"][domain] = False
log.info(f"cooking NS replies to point to {options.fakens} not matching: {domain}")
kitchen.CONFIG["NS"]['*'] = fakealias
else:
if fakeip:
kitchen.CONFIG["A"]['*'] = fakeip
log.info(f"cooking all A replies to point to {fakeip}")
if fakeipv6:
kitchen.CONFIG["AAAA"]['*'] = fakeipv6
log.info(f"cooking all AAAA replies to point to {fakeipv6}")
if fakemail:
kitchen.CONFIG["MX"]['*'] = fakemail
log.info(f"cooking all MX replies to point to {fakemail}")
if fakealias:
kitchen.CONFIG["CNAME"]['*'] = fakealias
log.info(f"cooking all CNAME replies to point to {fakealias}")
if fakens:
kitchen.CONFIG["NS"]['*'] = fakens
log.info(f"cooking all NS replies to point to {fakens}")
# Proxy all DNS requests
if not options.fakeip and not options.fakeipv6 and not options.fakemail and not options.fakealias and not options.fakens and not options.file:
log.info("running in full proxy mode as no parameters were specified")
if options.logfile:
fh = logging.handlers.WatchedFileHandler(options.logfile)
fh.setFormatter(plain_formatter)
fh.setLevel(
logging.INFO
if not options.verbose
else logging.DEBUG
)
log.addHandler(fh)
# Launch DNSChef
asyncio.run(start_server(
interface=options.interface,
nameservers=nameservers,
tcp=options.tcp,
ipv6=options.ipv6,
port=options.port
))
if __name__ == "__main__":
main()
================================================
FILE: dnschef/api.py
================================================
from dnschef import __version__
from dnschef import kitchen
from dnschef.protocols import start_server
from dnschef.utils import header, parse_config_file
from dnschef.logger import (
log,
plain_formatter,
json_capture_formatter,
capturer
)
from enum import Enum
from typing import Optional, List
from fastapi import FastAPI
from pydantic_settings import BaseSettings
from pydantic import BaseModel, FilePath #,IPvAnyAddress
from dnslib import RDMAP
import logging
import logging.handlers
import asyncio
DnsQueryType = Enum("DnsQueryType", {r:r for r in RDMAP.keys()})
class Record(BaseModel):
type: DnsQueryType
domain: str
value: str
class Settings(BaseSettings):
interface: str = "127.0.0.1"
nameservers: List[str] = [ "8.8.8.8" ]
ipv6: bool = False
tcp: bool = False
port: int = 53
configfile: FilePath = "dnschef.toml"
settings = Settings()
app = FastAPI(
title='DNSChef-NG',
version=__version__
)
@app.on_event("startup")
async def startup_event():
print(header)
kitchen.CONFIG = parse_config_file(settings.configfile)
# Log to file
fh = logging.handlers.WatchedFileHandler("dnschef.log")
fh.setFormatter(plain_formatter)
log.addHandler(fh)
# This will effectively duplicate all logs and save them in capturer.entries in JSON format
jh = logging.StreamHandler()
jh.setFormatter(json_capture_formatter)
log.addHandler(jh)
# Launch DNSChef
asyncio.create_task(
start_server(
interface=settings.interface,
nameservers=settings.nameservers,
tcp=settings.tcp,
ipv6=settings.ipv6,
port=settings.port
)
)
"""
@app.on_event("shutdown")
async def shutdown_event():
dns_chef_coroutine.cancel()
log.debug("Shutting down DNSChef API")
"""
@app.put("/")
async def add_record(record: Record):
kitchen.CONFIG[record.type.value][record.domain] = record.value
return 200
@app.delete("/")
async def delete_record(record: Record):
del kitchen.CONFIG[record.type.value][record.domain]
return 200
@app.get("/")
async def get_records():
return kitchen.CONFIG
@app.get("/logs")
async def get_logs(type: Optional[DnsQueryType] = None, name: Optional[str] = None):
events = ['cooking response', 'proxying response']
if not type and not name:
return capturer.entries
if type and name:
filter_expression = lambda l: l['event'] in events and l['type'] == type.value and name in l['name']
elif type:
filter_expression = lambda l: l['event'] in events and l['type'] == type.value
elif name:
filter_expression = lambda l: l['event'] in events and name in l['name']
return list(
filter(
filter_expression,
capturer.entries
)
)
================================================
FILE: dnschef/kitchen.py
================================================
from dnslib import *
from ipaddress import IPv4Address, IPv6Address
from dnschef.logger import log
import difflib
import fnmatch
import base64
import io
import itertools
import pathlib
import asyncio
import random
import time
CONFIG = {r: {} for r in RDMAP.keys()}
def chunk_string(string_to_chunk: str, chunk_size: int):
data = io.StringIO(string_to_chunk)
while True:
piece = data.read(chunk_size)
if not piece:
break
yield piece
def chunk_file(file_path: pathlib.Path, chunk_size: int):
with file_path.open('rb') as f:
while True:
piece = f.read(chunk_size)
if not piece:
break
yield piece
def get_file_chunk(file_path, chunk_index, chunk_size):
return next(itertools.islice(
chunk_file(file_path, chunk_size),
chunk_index,
chunk_index + 1
), b'')
async def stage_file(qname, record, chunk_size: int):
loop = asyncio.get_event_loop()
file_to_stage = pathlib.Path(record['file'])
if file_to_stage.exists() and file_to_stage.is_file():
chunk_index = int(''.join([ c for c in qname.split('.')[0] if c.isdigit() ]))
file_chunk = await loop.run_in_executor(None, get_file_chunk, file_to_stage, chunk_index, chunk_size)
return file_chunk
class DNSKitchen:
async def do_default(self, addr, qname, qtype, record):
if record[-1] == ".": record = record[:-1]
return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](record))
async def do_A(self, addr, qname, qtype, record):
if isinstance(record, dict):
chunk_size = record.get('chunk_size', 4)
if chunk_size > 4:
log.warning(f"chunk_size {chunk_size} is too large for A record, defaulting to 4")
chunk_size = 4
record = await stage_file(qname, record, chunk_size)
if record and len(record) < 4:
record = record.ljust(4, b'\x00')
if record:
ipv4_hex_tuple = list(map(int, IPv4Address(record).packed))
return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](ipv4_hex_tuple))
async def do_TXT(self, addr, qname, qtype, record):
if isinstance(record, dict):
chunk_size = record.get('chunk_size')
prefix = random.choice(record.get('response_prefix_pool', ['']))
response_format = record.get('response_format', '{prefix}{chunk}')
space_left = 255 - len(response_format.format(prefix=prefix, chunk=''))
max_data_len = ( space_left // 4 ) * 3
if chunk_size:
max_data_len = min(chunk_size, max_data_len)
if chunk_size > max_data_len:
log.warning(f"chunk_size {chunk_size} is too large for the TXT record, defaulting to {max_data_len}")
record = await stage_file(qname, record, chunk_size=max_data_len)
if record:
record = response_format.format(prefix=prefix, chunk=base64.b64encode(record).decode())
if record:
# dnslib doesn't like trailing dots
record = record.rstrip('.')
return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](record))
async def do_AAAA(self, addr, qname, qtype, record):
if isinstance(record, dict):
chunk_size = record.get('chunk_size', 16)
if chunk_size > 16:
log.warning(f"chunk_size {chunk_size} is too large for AAAA record, defaulting to 16")
chunk_size = 16
record = await stage_file(qname, record, chunk_size)
if record and len(record) < 16:
record = record.ljust(16, b'\x00')
if record:
ipv6_hex_tuple = list(map(int, IPv6Address(record).packed))
return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](ipv6_hex_tuple))
async def do_HTTPS(self, addr, qname, qtype, record):
kv_pairs = record.split(" ")
mydata = RDMAP[qtype].fromZone(kv_pairs)
return RR(qname, getattr(QTYPE, qtype), rdata=mydata)
async def do_SOA(self, addr, qname, qtype, record):
mname, rname, t1, t2, t3, t4, t5 = record.split(" ")
times = tuple([int(t) for t in [t1, t2, t3, t4, t5]])
# dnslib doesn't like trailing dots
if mname[-1] == ".": mname = mname[:-1]
if rname[-1] == ".": rname = rname[:-1]
return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](mname, rname, times))
async def do_NAPTR(self, addr, qname, qtype, record):
order, preference, flags, service, regexp, replacement = list(map(lambda x: x.encode(), record.split(" ")))
order = int(order)
preference = int(preference)
# dnslib doesn't like trailing dots
if replacement[-1] == ".": replacement = replacement[:-1]
return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](order, preference, flags, service, regexp, DNSLabel(replacement)))
async def do_SRV(self, addr, qname, qtype, record):
priority, weight, port, target = record.split(" ")
priority = int(priority)
weight = int(weight)
port = int(port)
if target[-1] == ".": target = target[:-1]
return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](priority, weight, port, target))
async def do_DNSKEY(self, addr, qname, qtype, record):
flags, protocol, algorithm, key = record.split(" ")
flags = int(flags)
protocol = int(protocol)
algorithm = int(algorithm)
key = base64.b64decode(("".join(key)).encode('ascii'))
return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](flags, protocol, algorithm, key))
async def do_RRSIG(self, addr, qname, qtype, record):
covered, algorithm, labels, orig_ttl, sig_exp, sig_inc, key_tag, name, sig = record.split(" ")
covered = getattr(QTYPE, covered) # NOTE: Covered QTYPE
algorithm = int(algorithm)
labels = int(labels)
orig_ttl = int(orig_ttl)
sig_exp = int(time.mktime(time.strptime(sig_exp + 'GMT', "%Y%m%d%H%M%S%Z")))
sig_inc = int(time.mktime(time.strptime(sig_inc + 'GMT', "%Y%m%d%H%M%S%Z")))
key_tag = int(key_tag)
if name[-1] == '.': name = name[:-1]
sig = base64.b64decode( ("".join(sig)).encode('ascii') )
return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](covered, algorithm, labels, orig_ttl, sig_exp, sig_inc, key_tag, name, sig))
# Find appropriate ip address to use for a queried name.
def findnametodns(self, qname, qtype):
# Make qname case insensitive
qname = qname.lower()
matched_domains = [
k for k,_ in CONFIG[qtype].items()
if (k == '*' or qname.count('.') == k.count('.')) and fnmatch.fnmatch(qname, k)
]
if matched_domains:
top_matched_domains = list(sorted(
matched_domains,
key=lambda domain: difflib.SequenceMatcher(a=domain, b=qname).quick_ratio(),
reverse=True
))
#return { qtype: { k:v for k,v in CONFIG[qtype].items() if k == top_matched_domains[0] } }
return CONFIG[qtype][top_matched_domains[0]]
async def we_cookin(self, logger, d, qtype, qname, addr):
# Create a custom response to the query
response = DNSRecord(
DNSHeader(id=d.header.id, bitmap=d.header.bitmap, qr=1, aa=1, ra=1),
q=d.q
)
cooked_reply = self.findnametodns(qname, qtype)
# Check if there is a fake record for the current request qtype
if CONFIG.get(qtype) and cooked_reply:
logger.info("cooking response")
response_func = getattr(
self,
f"do_{qtype}",
self.do_default
)
answer = await response_func(addr, qname, qtype, cooked_reply)
if answer:
response.add_answer(answer)
return response
================================================
FILE: dnschef/logger.py
================================================
from rich.traceback import install
import logging
import logging.handlers
import structlog
install(show_locals=True)
capturer = structlog.testing.LogCapture()
timestamper = structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S")
plain_formatter = structlog.stdlib.ProcessorFormatter(
processors = [
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
#structlog.stdlib.add_log_level,
structlog.dev.ConsoleRenderer(colors=False),
]
)
colored_formatter = structlog.stdlib.ProcessorFormatter(
processors = [
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
#structlog.stdlib.add_log_level,
structlog.dev.ConsoleRenderer(colors=True, exception_formatter=structlog.dev.rich_traceback),
]
)
debug_formatter = structlog.stdlib.ProcessorFormatter(
processors = [
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.stdlib.add_log_level,
#structlog.stdlib.add_logger_name,
structlog.dev.ConsoleRenderer(colors=True, exception_formatter=structlog.dev.rich_traceback),
]
)
json_capture_formatter = structlog.stdlib.ProcessorFormatter(
processors = [
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
capturer,
structlog.processors.JSONRenderer(),
]
)
sh = logging.StreamHandler()
sh.setFormatter(colored_formatter)
dnschef_logger = logging.getLogger("dnschef")
dnschef_logger.setLevel(logging.INFO)
dnschef_logger.addHandler(sh)
structlog.configure(
processors=[
#structlog.stdlib.filter_by_level,
#structlog.stdlib.add_logger_name,
#structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
timestamper,
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
#structlog.processors.UnicodeDecoder(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
log = structlog.get_logger("dnschef")
================================================
FILE: dnschef/protocols.py
================================================
import asyncio
import socket
import re
import random
import functools
import enum
from dnslib import DNSRecord, QR, QTYPE
from typing import List
from dnschef.logger import log
from dnschef import kitchen
class ClientProtocol(enum.Enum):
UDP = 1
TCP = 2
class UdpDnsClientProtocol:
def __init__(self, request, on_con_lost):
self.transport = None
self.request = request
self.on_con_lost = on_con_lost
def connection_made(self, transport):
self.transport = transport
log.debug('sending', request=self.request)
self.transport.sendto(self.request)
def datagram_received(self, data, addr):
log.debug("received", addr=addr, data=data)
self.reply = data
self.transport.close()
def error_received(self, exc):
log.exception('error received')
def connection_lost(self, exc):
log.debug("connection closed")
self.on_con_lost.set_result(True)
class TcpDnsClientProtocol(asyncio.Protocol):
def __init__(self, request, on_con_lost):
self.request = request
self.on_con_lost = on_con_lost
def connection_made(self, transport):
self.transport = transport
log.debug('sending', request=self.request)
self.transport.write(self.request)
def data_received(self, data):
addr = self.transport.get_extra_info('peername')
log.debug("received", addr=addr, data=data)
self.reply = data
self.transport.close()
def connection_lost(self, exc):
log.debug("connection closed")
# The socket has been closed
self.on_con_lost.set_result(True)
# Obtain a response from a real DNS server.
async def proxy_request(request, host, protocol: ClientProtocol, port: int = 53):
loop = asyncio.get_running_loop()
on_con_lost = loop.create_future()
if protocol == ClientProtocol.UDP:
transport, protocol = await loop.create_datagram_endpoint(
lambda: UdpDnsClientProtocol(request, on_con_lost),
remote_addr=(host, int(port)))
else:
transport, protocol = await loop.create_connection(
lambda: TcpDnsClientProtocol(request, on_con_lost),
host=host,
port=int(port)
)
try:
await on_con_lost
finally:
transport.close()
return protocol.reply
class UdpDnsServerProtocol:
def __init__(self, nameservers, dns_kitchen):
self.nameservers = [ re.split('[:#]', ns) for ns in nameservers ]
self.dns_kitchen = dns_kitchen
def connection_made(self, transport):
self.transport = transport
def datagram_received(self, data, addr):
logger = log.bind(address=addr[0], proto="udp")
try:
d = DNSRecord.parse(data)
except Exception:
logger.error("invalid DNS request")
else:
# Only Process DNS Queries
if not QR[d.header.qr] == "QUERY":
logger.warning("received a non-query DNS request")
return
qtype = QTYPE[d.q.qtype]
qname = str(d.q.qname).rstrip('.')
logger = logger.bind(name=qname, type=qtype)
def _cooked_cb(future):
response = future.result()
if response:
logger.debug("dns packet", packet=response.pack())
self.transport.sendto(response.pack(), addr)
else:
logger.info("proxying response")
task = asyncio.create_task(
proxy_request(
data,
*random.choice(self.nameservers),
protocol=ClientProtocol.UDP
)
)
task.add_done_callback(functools.partial(
lambda c, t, a: t.sendto(c.result(), a), t=self.transport, a=addr
))
task = asyncio.create_task(self.dns_kitchen.we_cookin(logger, d, qtype, qname, addr))
task.add_done_callback(_cooked_cb)
class TcpDnsServerProtocol(asyncio.Protocol):
def __init__(self, nameservers, dns_kitchen):
self.nameservers = [ re.split('[:#]', ns) for ns in nameservers ]
self.dns_kitchen = dns_kitchen
def connection_made(self, transport):
self.transport = transport
def data_received(self, data):
addr = self.transport.get_extra_info('peername')
logger = log.bind(address=addr[0], proto="tcp")
try:
d = DNSRecord.parse(data[2:])
except Exception:
logger.error("invalid DNS request")
else:
# Only Process DNS Queries
if not QR[d.header.qr] == "QUERY":
logger.warning("received a non-query DNS request")
return
qtype = QTYPE[d.q.qtype]
qname = str(d.q.qname).rstrip('.')
logger = logger.bind(name=qname, type=qtype)
def _cooked_cb(future):
response = future.result()
if response:
logger.debug("dns packet", packet=response.pack())
self.transport.write(
len(response.pack()).to_bytes(2, byteorder='big') + response.pack()
)
else:
logger.info("proxying response")
task = asyncio.create_task(
proxy_request(
data,
*random.choice(self.nameservers),
protocol=ClientProtocol.TCP
)
)
task.add_done_callback(functools.partial(
lambda c, t: t.write(c.result()), t=self.transport
))
task = asyncio.create_task(self.dns_kitchen.we_cookin(logger, d, qtype, qname, addr))
task.add_done_callback(_cooked_cb)
async def start_server(interface: str, nameservers: List[str], tcp: bool = False, ipv6: bool = False, port: int = 53):
loop = asyncio.get_running_loop()
family= socket.AF_INET if not ipv6 else socket.AF_INET6
if tcp:
server = await loop.create_server(
lambda: TcpDnsServerProtocol(nameservers, kitchen.DNSKitchen()),
host=interface,
port=int(port),
family=family
)
transport, protocol = await loop.create_datagram_endpoint(
lambda: UdpDnsServerProtocol(nameservers, kitchen.DNSKitchen()),
local_addr=(interface, int(port)),
family=family
)
log.info("DNSChef is active", interface=interface, tcp=tcp, ipv6=ipv6, port=port, nameservers=nameservers)
while True:
await asyncio.sleep(1)
================================================
FILE: dnschef/utils.py
================================================
import tomllib
from dnschef import __version__
from dnschef.logger import log
from dnslib import RDMAP
header = " _ _ __ \n"
header += " | | v{} | | / _| \n".format(__version__)
header += " __| |_ __ ___ ___| |__ ___| |_ \n"
header += " / _` | '_ \/ __|/ __| '_ \ / _ \ _|\n"
header += " | (_| | | | \__ \ (__| | | | __/ | \n"
header += " \__,_|_| |_|___/\___|_| |_|\___|_| \n"
header += " @iphelix // @byt3bl33d3r \n"
def parse_config_file(config_file: str = "dnschef.toml"):
log.debug("Parsing config file", path=config_file)
with open(config_file, 'rb') as f:
config = tomllib.load(f)
for record, domains in config.items():
if record not in RDMAP:
log.warning(f"DNS record '{record}' is not supported. Contents will be ignored.")
continue
for domain, values in domains.items():
if isinstance(values, dict):
log.info("cooking file staging", section=record, domain=domain.lower(), file=values['file'])
else:
log.info("cooking replies", section=record, domain=domain.lower(), reply=values)
return config
================================================
FILE: dnschef.toml
================================================
[A] # Queries for IPv4 address records
"*.thesprawl.org" = "100.100.100.100"
"*.test.thesprawl.org" = "127.0.0.1"
"*.*.thesprawl.org" = "1.1.1.1"
"c.*.*.thesprawl.org" = "1.1.2.2"
"fuck.shit.com" = "192.168.0.1"
"*.wat.org" = { file = "./requirements.txt", chunk_size = 122 }
[AAAA] # Queries for IPv6 address records
"*.thesprawl.org" = "2001:db8::1"
"*.wat.org" = { file = "./requirements.txt", chunk_size = 122 }
[MX] # Queries for mail server records
"*.thesprawl.org" = "mail.fake.com"
[NS] # Queries for mail server records
"*.thesprawl.org" = "ns.fake.com"
[CNAME] # Queries for alias records
"*.thesprawl.org" = "www.fake.com"
[TXT] # Queries for text records
"*.thesprawl.org" = "fake message"
"ok.thesprawl.org" = "fake message"
"*.something.wattahog.org" = "fuck off"
"ns*.shit.fuck.org" = { file = "./requirements.txt", chunk_size = 189, response_format = "{prefix}test-{chunk}", response_prefix_pool = ["atlassian-domain-verification=", "onetrust-domain-verification=", "docusign=" ] }
[TXT."*.wattahog.org"]
file = "./requirements.txt"
chunk_size = 189
response_format = "{prefix}test-{chunk}"
response_prefix_pool = [ "atlassian-domain-verification=", "onetrust-domain-verification=" , "docusign=" ]
[PTR]
"*.2.0.192.in-addr.arpa" = "fake.com"
[SOA]
# FORMAT: mname rname t1 t2 t3 t4 t5
"*.thesprawl.org" = "ns.fake.com. hostmaster.fake.com. 1 10800 3600 604800 3600"
[NAPTR]
# FORMAT: order preference flags service regexp replacement
"*.thesprawl.org" = "100 10 U E2U+sip !^.*$!sip:customer-service@fake.com! ."
[SRV]
# FORMAT: priority weight port target
"*.*.thesprawl.org" = "0 5 5060 sipserver.fake.com"
[DNSKEY]
# FORMAT: flags protocol algorithm base64(key)
"*.thesprawl.org" = "256 3 5 AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQeogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU/TpPSEDhm2SNKLijfUppn1UaNvv4w=="
[RRSIG]
# FORMAT: covered algorithm labels labels orig_ttl sig_exp sig_inc key_tag name base64(sig)
"*.thesprawl.org" = "A 5 3 86400 20030322173103 20030220173103 2642 thesprawl.org. oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTrPYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6oB9wfuh3DTJXUAfI/M0zmO/zz8bW0Rznl8O3tGNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkGJ5D6fwFm8nN+6pBzeDQfsS3Ap3o="
[HTTPS]
# FORMAT: priority target key=value pairs
"*.thesprawl.org" = "1 . alpn=h2 ipv4hint=127.0.0.1 ipv6hint=::1"
================================================
FILE: docker-compose.yml
================================================
version: "3"
services:
dnschef:
image: dnschef:latest
container_name: dnschef
ports:
- "53:53/udp"
- "53:53/tcp"
expose:
- "80"
volumes:
- ./dnschef.toml:/etc/dnschef.toml
environment:
- INTERFACE=0.0.0.0
- NAMESERVERS=8.8.8.8
- PORT=53
- TCP=false
- IPV6=false
- CONFIGFILE=/etc/dnschef.toml
================================================
FILE: pyproject.toml
================================================
[tool.poetry]
name = "dnschef-ng"
version = "0.7.2"
description = "A highly configurable DNS proxy for Penetration Testers and Malware Analysts"
authors = ["iphelix <iphelix@thesprawl.org>","byt3bl33d3r <byt3bl33d3r@pm.me>"]
readme = "README.md"
license = "BSD-3-Clause"
packages = [{include = "dnschef"}]
classifiers = [
"Environment :: Console",
"Programming Language :: Python :: 3",
"Topic :: Security",
]
exclude = ["tests"]
[tool.poetry.scripts]
dnschef = 'dnschef.__main__:main'
dnschef-ng = 'dnschef.__main__:main'
[tool.poetry.dependencies]
python = "^3.11"
dnslib = "^0.9.23"
rich = "^13.5.3"
structlog = "^23.1.0"
fastapi = { version = "^0.103.1", optional = true }
uvicorn = { version = "^0.23.2", optional = true }
pydantic-settings = { version = "^2.0.3", optional = true }
[tool.poetry.extras]
api = ["fastapi", "uvicorn", "pydantic-settings"]
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.2"
pytest-asyncio = "^0.21.1"
poetry-plugin-export = "^1.6.0"
ruff = "^0.1.6"
dnspython = "^2.4.2"
pytest-cov = "^4.1.0"
httpx = "^0.25.1"
[tool.pytest.ini_options]
addopts = "--cov=dnschef"
log_cli = false
log_cli_level = "INFO"
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
filterwarnings = [
# note the use of single quote below to denote "raw" strings in TOML
'ignore:`general_plain_validator_function` is deprecated',
]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
================================================
FILE: requirements-api.txt
================================================
annotated-types==0.6.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \
--hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d
anyio==3.7.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780 \
--hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5
click==8.1.7 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and platform_system == "Windows" \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
dnslib==0.9.23 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:310196d3e38ce2051b61eebbd2f1d08fcc934fa3360f22031864d16efe8bca77 \
--hash=sha256:46137e8ef6ef52b24a16d47e0786a99dd103ab1e71eea616f21371accbccc557 \
--hash=sha256:9eb851ac721eea51834d43795478ac9b48272c61ba97cc4a160668b50aff39ec
fastapi==0.103.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:3270de872f0fe9ec809d4bd3d4d890c6d5cc7b9611d721d6438f9dacc8c4ef2e \
--hash=sha256:75a11f6bfb8fc4d2bec0bd710c2d5f2829659c0e8c0afd5560fdda6ce25ec653
h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
idna==3.4 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
--hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
markdown-it-py==3.0.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
--hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
mdurl==0.1.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
pydantic-core==2.14.5 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b \
--hash=sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b \
--hash=sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d \
--hash=sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8 \
--hash=sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124 \
--hash=sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189 \
--hash=sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c \
--hash=sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d \
--hash=sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f \
--hash=sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520 \
--hash=sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4 \
--hash=sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6 \
--hash=sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955 \
--hash=sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3 \
--hash=sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b \
--hash=sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a \
--hash=sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68 \
--hash=sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3 \
--hash=sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd \
--hash=sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de \
--hash=sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b \
--hash=sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634 \
--hash=sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7 \
--hash=sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459 \
--hash=sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7 \
--hash=sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3 \
--hash=sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331 \
--hash=sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf \
--hash=sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d \
--hash=sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36 \
--hash=sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59 \
--hash=sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937 \
--hash=sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc \
--hash=sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093 \
--hash=sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753 \
--hash=sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706 \
--hash=sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca \
--hash=sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260 \
--hash=sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997 \
--hash=sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588 \
--hash=sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71 \
--hash=sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb \
--hash=sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e \
--hash=sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69 \
--hash=sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5 \
--hash=sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07 \
--hash=sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1 \
--hash=sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0 \
--hash=sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd \
--hash=sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8 \
--hash=sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944 \
--hash=sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26 \
--hash=sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda \
--hash=sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4 \
--hash=sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9 \
--hash=sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00 \
--hash=sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe \
--hash=sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6 \
--hash=sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada \
--hash=sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4 \
--hash=sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7 \
--hash=sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325 \
--hash=sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4 \
--hash=sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b \
--hash=sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88 \
--hash=sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04 \
--hash=sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863 \
--hash=sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0 \
--hash=sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911 \
--hash=sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b \
--hash=sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e \
--hash=sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144 \
--hash=sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5 \
--hash=sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720 \
--hash=sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab \
--hash=sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d \
--hash=sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789 \
--hash=sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec \
--hash=sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2 \
--hash=sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db \
--hash=sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f \
--hash=sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef \
--hash=sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3 \
--hash=sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209 \
--hash=sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc \
--hash=sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651 \
--hash=sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8 \
--hash=sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e \
--hash=sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66 \
--hash=sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7 \
--hash=sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550 \
--hash=sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd \
--hash=sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405 \
--hash=sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27 \
--hash=sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093 \
--hash=sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077 \
--hash=sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113 \
--hash=sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3 \
--hash=sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6 \
--hash=sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf \
--hash=sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed \
--hash=sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88 \
--hash=sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe \
--hash=sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18 \
--hash=sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867
pydantic-settings==2.1.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c \
--hash=sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a
pydantic==2.5.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0 \
--hash=sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd
pygments==2.17.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367
python-dotenv==1.0.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba \
--hash=sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a
rich==13.7.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa \
--hash=sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235
sniffio==1.3.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \
--hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384
starlette==0.27.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75 \
--hash=sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91
structlog==23.2.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:16a167e87b9fa7fae9a972d5d12805ef90e04857a93eba479d4be3801a6a1482 \
--hash=sha256:334666b94707f89dbc4c81a22a8ccd34449f0201d5b1ee097a030b577fa8c858
typing-extensions==4.8.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \
--hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef
uvicorn==0.23.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53 \
--hash=sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a
================================================
FILE: requirements-dev.txt
================================================
anyio==3.7.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780 \
--hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5
build==1.0.3 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b \
--hash=sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f
cachecontrol[filecache]==0.13.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:95dedbec849f46dda3137866dc28b9d133fc9af55f5b805ab1291833e4457aa4 \
--hash=sha256:f012366b79d2243a6118309ce73151bf52a38d4a5dac8ea57f09bd29087e506b
certifi==2023.11.17 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1 \
--hash=sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474
cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0" and (sys_platform == "darwin" or sys_platform == "linux") \
--hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \
--hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \
--hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \
--hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \
--hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \
--hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \
--hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \
--hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \
--hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \
--hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \
--hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \
--hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \
--hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \
--hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \
--hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \
--hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \
--hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \
--hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \
--hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \
--hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \
--hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \
--hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \
--hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \
--hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \
--hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \
--hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \
--hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \
--hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \
--hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \
--hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \
--hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \
--hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \
--hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \
--hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \
--hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \
--hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \
--hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \
--hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \
--hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \
--hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \
--hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \
--hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \
--hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \
--hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \
--hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \
--hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \
--hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \
--hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \
--hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \
--hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \
--hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \
--hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357
charset-normalizer==3.3.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
--hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \
--hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \
--hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \
--hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \
--hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \
--hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \
--hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \
--hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \
--hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \
--hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \
--hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \
--hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \
--hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \
--hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \
--hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \
--hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \
--hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \
--hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \
--hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \
--hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \
--hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \
--hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \
--hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \
--hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \
--hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \
--hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \
--hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \
--hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \
--hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \
--hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \
--hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \
--hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \
--hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \
--hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \
--hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \
--hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \
--hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \
--hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \
--hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \
--hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \
--hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \
--hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \
--hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \
--hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \
--hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \
--hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \
--hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \
--hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \
--hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \
--hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \
--hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \
--hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \
--hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \
--hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \
--hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \
--hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \
--hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \
--hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \
--hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \
--hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \
--hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \
--hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \
--hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \
--hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \
--hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \
--hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \
--hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \
--hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \
--hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \
--hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \
--hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \
--hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \
--hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \
--hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \
--hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \
--hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \
--hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \
--hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \
--hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \
--hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \
--hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \
--hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \
--hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \
--hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \
--hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \
--hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \
--hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \
--hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
--hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
cleo==2.1.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523 \
--hash=sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e
colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and (sys_platform == "win32" or os_name == "nt") \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
coverage[toml]==7.3.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1 \
--hash=sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63 \
--hash=sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9 \
--hash=sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312 \
--hash=sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3 \
--hash=sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb \
--hash=sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25 \
--hash=sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92 \
--hash=sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda \
--hash=sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148 \
--hash=sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6 \
--hash=sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216 \
--hash=sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a \
--hash=sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640 \
--hash=sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836 \
--hash=sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c \
--hash=sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f \
--hash=sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2 \
--hash=sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901 \
--hash=sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed \
--hash=sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a \
--hash=sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074 \
--hash=sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc \
--hash=sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84 \
--hash=sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083 \
--hash=sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f \
--hash=sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c \
--hash=sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c \
--hash=sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637 \
--hash=sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2 \
--hash=sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82 \
--hash=sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f \
--hash=sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce \
--hash=sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef \
--hash=sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f \
--hash=sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611 \
--hash=sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c \
--hash=sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76 \
--hash=sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9 \
--hash=sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce \
--hash=sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9 \
--hash=sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf \
--hash=sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf \
--hash=sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9 \
--hash=sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6 \
--hash=sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2 \
--hash=sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a \
--hash=sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a \
--hash=sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf \
--hash=sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738 \
--hash=sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a \
--hash=sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4
crashtest==0.4.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce \
--hash=sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5
cryptography==41.0.5 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "linux" \
--hash=sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf \
--hash=sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84 \
--hash=sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e \
--hash=sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8 \
--hash=sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7 \
--hash=sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1 \
--hash=sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88 \
--hash=sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86 \
--hash=sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179 \
--hash=sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81 \
--hash=sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20 \
--hash=sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548 \
--hash=sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d \
--hash=sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d \
--hash=sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5 \
--hash=sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1 \
--hash=sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147 \
--hash=sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936 \
--hash=sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797 \
--hash=sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696 \
--hash=sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72 \
--hash=sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da \
--hash=sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723
distlib==0.3.7 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \
--hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8
dnslib==0.9.23 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:310196d3e38ce2051b61eebbd2f1d08fcc934fa3360f22031864d16efe8bca77 \
--hash=sha256:46137e8ef6ef52b24a16d47e0786a99dd103ab1e71eea616f21371accbccc557 \
--hash=sha256:9eb851ac721eea51834d43795478ac9b48272c61ba97cc4a160668b50aff39ec
dnspython==2.4.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8 \
--hash=sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984
dulwich==0.21.6 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:008ff08629ab16d3638a9f36cfc6f5bd74b4d594657f2dc1583d8d3201794571 \
--hash=sha256:18697b58e0fc5972de68b529b08ac9ddda3f39af27bcf3f6999635ed3da7ef68 \
--hash=sha256:1fedd924763a5d640348db43a267a394aa80d551228ad45708e0b0cc2130bb62 \
--hash=sha256:22798e9ba59e32b8faff5d9067e2b5a308f6b0fba9b1e1e928571ad278e7b36c \
--hash=sha256:24ad45928a65f39ea0f451f9989b7aaedba9893d48c3189b544a70c6a1043f71 \
--hash=sha256:28acbd08d6b38720d99cc01da9dd307a2e0585e00436c95bcac6357b9a9a6f76 \
--hash=sha256:28c9724a167c84a83fc6238e0781f4702b5fe8c53ede31604525fb1a9d1833f4 \
--hash=sha256:2a3fc071e5b14f164191286f7ffc02f60fe8b439d01fad0832697cc08c2237dd \
--hash=sha256:30fbe87e8b51f3813c131e2841c86d007434d160bd16db586b40d47f31dd05b0 \
--hash=sha256:32d3a35caad6879d04711b358b861142440a543f5f4e02df67b13cbcd57f84a6 \
--hash=sha256:32d7acfe3fe2ce4502446d8f7a5ab34cfd24c9ff8961e60337638410906a8fbb \
--hash=sha256:3b1682e8e826471ea3c22b8521435e93799e3db8ad05dd3c8f9b1aaacfa78147 \
--hash=sha256:40623cc39a3f1634663d22d87f86e2e406cc8ff17ae7a3edc7fcf963c288992f \
--hash=sha256:4e09d0b4e985b371aa6728773781b19298d361a00772e20f98522868cf7edc6f \
--hash=sha256:4fdc2f081bc3e9e120079c2cea4be213e3f127335aca7c0ab0c19fe791270caa \
--hash=sha256:513d045e74307eeb31592255c38f37042c9aa68ce845a167943018ab5138b0e3 \
--hash=sha256:54342cf96fe8a44648505c65f23d18889595762003a168d67d7263df66143bd2 \
--hash=sha256:5d2ccf3d355850674f75655154a6519bf1f1664176c670109fa7041019b286f9 \
--hash=sha256:5e58171a5d70f7910f73d25ff82a058edff09a4c1c3bd1de0dc6b1fbc9a42c3e \
--hash=sha256:6592ef2d16ac61a27022647cf64a048f5be6e0a6ab2ebc7322bfbe24fb2b971b \
--hash=sha256:6c91e1ed20d3d9a6aaaed9e75adae37272b3fcbcc72bab1eb09574806da88563 \
--hash=sha256:6fe957564108f74325d0d042d85e0c67ef470921ca92b6e7d330c7c49a3b9c1d \
--hash=sha256:7f89bee4c97372e8aaf8ffaf5899f1bcd5184b5306d7eaf68738c1101ceba10e \
--hash=sha256:81d10aa50c0a9a6dd495990c639358e3a3bbff39e17ff302179be6e93b573da7 \
--hash=sha256:81e237a6b1b20c79ef62ca19a8fb231f5519bab874b9a1c2acf9c05edcabd600 \
--hash=sha256:847bb52562a211b596453a602e75739350c86d7edb846b5b1c46896a5c86b9bb \
--hash=sha256:8b84450766a3b151c3676fec3e3ed76304e52a84d5d69ade0f34fff2782c1b41 \
--hash=sha256:8dfb50b3915e223a97f50fbac0dbc298d5fffeaac004eeeb3d552c57fe38416f \
--hash=sha256:99577b2b37f64bc87280079245fb2963494c345d7db355173ecec7ab3d64b949 \
--hash=sha256:a1ac20dfcfd6057efb8499158d23f2c059f933aefa381e192100e6d8bc25d562 \
--hash=sha256:a2912c8a845c8ccbc79d068a89db7172e355adeb84eb31f062cd3a406d528b30 \
--hash=sha256:a3da632648ee27b64bb5b285a3a94fddf297a596891cca12ac0df43c4f59448f \
--hash=sha256:a64eca1601e79c16df78afe08da9ac9497b934cbc5765990ca7d89a4b87453d9 \
--hash=sha256:a780e2a0ff208c4f218e72eff8d13f9aff485ff9a6f3066c22abe4ec8cec7dcd \
--hash=sha256:a89b19f4960e759915dbc23a4dd0abc067b55d8d65e9df50961b73091b87b81a \
--hash=sha256:a9b52a08d49731375662936d05a12c4a64a6fe0ce257111f62638e475fb5d26d \
--hash=sha256:a9b6f8a16f32190aa88c37ef013858b3e01964774bc983900bd0d74ecb6576e6 \
--hash=sha256:b0545f0fa9444a0eb84977d08e302e3f55fd7c34a0466ec28bedc3c839b2fc1f \
--hash=sha256:b1c9e55233f19cd19c484f607cd90ab578ac50ebfef607f77e3b35c2b6049470 \
--hash=sha256:bf469cd5076623c2aad69d01ce9d5392fcb38a5faef91abe1501be733453e37d \
--hash=sha256:bf90f2f9328a82778cf85ab696e4a7926918c3f315c75fc432ba31346bfa89b7 \
--hash=sha256:c04df87098053b7767b46fc04b7943d75443f91c73560ca50157cdc22e27a5d3 \
--hash=sha256:c2f2683e0598f7c7071ef08a0822f062d8744549a0d45f2c156741033b7e3d7d \
--hash=sha256:c816be529680659b6a19798287b4ec6de49040f58160d40b1b2934fd6c28e93f \
--hash=sha256:ceabe8f96edfb9183034a860f5dc77586700b517457032867b64a03c44e5cf96 \
--hash=sha256:cef50c0a19f322b7150248b8fa0862ce1652dec657e340c4020573721e85f215 \
--hash=sha256:d7cd9fb896c65e4c28cb9332f2be192817805978dd8dc299681c4fe83c631158 \
--hash=sha256:d9002094198e57e88fe77412d3aa64dd05978046ae725a16123ba621a7704628 \
--hash=sha256:daa3584beabfcf0da76df57535a23c80ff6d8ccde6ddbd23bdc79d317a0e20a7 \
--hash=sha256:e07f145c7b0d82a9f77d157f493a61900e913d1c1f8b1f40d07d919ffb0929a4 \
--hash=sha256:e0dee3840c3c72e1d60c8f87a7a715d8eac023b9e1b80199d97790f7a1c60d9c \
--hash=sha256:e1ac882afa890ef993b8502647e6c6d2b3977ce56e3fe80058ce64607cbc7107 \
--hash=sha256:e8ed878553f0b76facbb620b455fafa0943162fe8e386920717781e490444efa \
--hash=sha256:ed2f1f638b9adfba862719693b371ffe5d58e94d552ace9a23dea0fb0db6f468 \
--hash=sha256:edc21c3784dd9d9b85abd9fe53f81a884e2cdcc4e5e09ada17287420d64cfd46 \
--hash=sha256:eee8aba4dec4d0a52737a8a141f3456229c87dcfd7961f8115786a27b6ebefed
fastjsonschema==2.19.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:b9fd1a2dd6971dbc7fee280a95bd199ae0dd9ce22beb91cc75e9c1c528a5170e \
--hash=sha256:e25df6647e1bc4a26070b700897b07b542ec898dd4f1f6ea013e7f6a88417225
filelock==3.13.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \
--hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c
h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
httpcore==1.0.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7 \
--hash=sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535
httpx==0.25.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a \
--hash=sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0
idna==3.4 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
--hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
importlib-metadata==6.8.0 ; python_version >= "3.11" and python_version < "3.12" \
--hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
--hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
installer==0.7.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53 \
--hash=sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631
jaraco-classes==3.3.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb \
--hash=sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621
jeepney==0.8.0 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "linux" \
--hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \
--hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755
keyring==24.3.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836 \
--hash=sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25
markdown-it-py==3.0.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
--hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
mdurl==0.1.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
more-itertools==10.1.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a \
--hash=sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6
msgpack==1.0.7 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862 \
--hash=sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d \
--hash=sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3 \
--hash=sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672 \
--hash=sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0 \
--hash=sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9 \
--hash=sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee \
--hash=sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46 \
--hash=sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524 \
--hash=sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819 \
--hash=sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc \
--hash=sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc \
--hash=sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1 \
--hash=sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82 \
--hash=sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81 \
--hash=sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6 \
--hash=sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d \
--hash=sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2 \
--hash=sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c \
--hash=sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87 \
--hash=sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84 \
--hash=sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e \
--hash=sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95 \
--hash=sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f \
--hash=sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b \
--hash=sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93 \
--hash=sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf \
--hash=sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61 \
--hash=sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c \
--hash=sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8 \
--hash=sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d \
--hash=sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c \
--hash=sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4 \
--hash=sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba \
--hash=sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415 \
--hash=sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee \
--hash=sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d \
--hash=sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9 \
--hash=sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075 \
--hash=sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f \
--hash=sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7 \
--hash=sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681 \
--hash=sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329 \
--hash=sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1 \
--hash=sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf \
--hash=sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c \
--hash=sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5 \
--hash=sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b \
--hash=sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5 \
--hash=sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e \
--hash=sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b \
--hash=sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad \
--hash=sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd \
--hash=sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7 \
--hash=sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002 \
--hash=sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc
packaging==23.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \
--hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7
pexpect==4.8.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \
--hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c
pkginfo==1.9.6 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \
--hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046
platformdirs==3.11.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \
--hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e
pluggy==1.3.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12 \
--hash=sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7
poetry-core==1.8.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:194832b24f3283e01c5402eae71a6aae850ecdfe53f50a979c76bf7aa5010ffa \
--hash=sha256:67a76c671da2a70e55047cddda83566035b701f7e463b32a2abfeac6e2a16376
poetry-plugin-export==1.6.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:091939434984267a91abf2f916a26b00cff4eee8da63ec2a24ba4b17cf969a59 \
--hash=sha256:2dce6204c9318f1f6509a11a03921fb3f461b201840b59f1c237b6ab454dabcf
poetry==1.7.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:03d3807a0fb3bc1028cc3707dfd646aae629d58e476f7e7f062437680741c561 \
--hash=sha256:b348a70e7d67ad9c0bd3d0ea255bc6df84c24cf4b16f8d104adb30b425d6ff32
ptyprocess==0.7.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
--hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0" and (sys_platform == "darwin" or sys_platform == "linux") \
--hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
--hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
pygments==2.17.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367
pyproject-hooks==1.0.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
--hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
pytest-asyncio==0.21.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d \
--hash=sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b
pytest-cov==4.1.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \
--hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a
pytest==7.4.3 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac \
--hash=sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5
pywin32-ctypes==0.2.2 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32" \
--hash=sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60 \
--hash=sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7
rapidfuzz==3.5.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:00be97f9219355945c46f37ac9fa447046e6f7930f7c901e5d881120d1695458 \
--hash=sha256:04e1e02b182283c43c866e215317735e91d22f5d34e65400121c04d5ed7ed859 \
--hash=sha256:089a7e96e5032821af5964d8457fcb38877cc321cdd06ad7c5d6e3d852264cb9 \
--hash=sha256:0fef4705459842ef8f79746d6f6a0b5d2b6a61a145d7d8bbe10b2e756ea337c8 \
--hash=sha256:1062425c8358a547ae5ebad148f2e0f02417716a571b803b0c68e4d552e99d32 \
--hash=sha256:120316824333e376b88b284724cfd394c6ccfcb9818519eab5d58a502e5533f0 \
--hash=sha256:12424a06ad9bd0cbf5f7cea1015e78d924a0034a0e75a5a7b39c0703dcd94095 \
--hash=sha256:1962d5ccf8602589dbf8e85246a0ee2b4050d82fade1568fb76f8a4419257704 \
--hash=sha256:1a047d6e58833919d742bbc0dfa66d1de4f79e8562ee195007d3eae96635df39 \
--hash=sha256:1a4a7832737f87583f3863dc62e6f56dd4a9fefc5f04a7bdcb4c433a0f36bb1b \
--hash=sha256:1d5a686ea258931aaa38019204bdc670bbe14b389a230b1363d84d6cf4b9dc38 \
--hash=sha256:1dd2542e5103fb8ca46500a979ae14d1609dcba11d2f9fe01e99eec03420e193 \
--hash=sha256:22877c027c492b7dc7e3387a576a33ed5aad891104aa90da2e0844c83c5493ef \
--hash=sha256:25510b5d142c47786dbd27cfd9da7cae5bdea28d458379377a3644d8460a3404 \
--hash=sha256:27689361c747b5f7b8a26056bc60979875323f1c3dcaaa9e2fec88f03b20a365 \
--hash=sha256:2bacce6bbc0362f0789253424269cc742b1f45e982430387db3abe1d0496e371 \
--hash=sha256:2cf9f2ed4a97b388cffd48d534452a564c2491f68f4fd5bc140306f774ceb63a \
--hash=sha256:2d876dba9a11fcf60dcf1562c5a84ef559db14c2ceb41e1ad2d93cd1dc085889 \
--hash=sha256:2da3a24c2f7dfca7f26ba04966b848e3bbeb93e54d899908ff88dfe3e1def9dc \
--hash=sha256:2fbaf546f15a924613f89d609ff66b85b4f4c2307ac14d93b80fe1025b713138 \
--hash=sha256:32d580df0e130ed85400ff77e1c32d965e9bc7be29ac4072ab637f57e26d29fb \
--hash=sha256:358a0fbc49343de20fee8ebdb33c7fa8f55a9ff93ff42d1ffe097d2caa248f1b \
--hash=sha256:365e544aba3ac13acf1a62cb2e5909ad2ba078d0bfc7d69b1f801dfd673b9782 \
--hash=sha256:40139552961018216b8cd88f6df4ecbbe984f907a62a5c823ccd907132c29a14 \
--hash=sha256:43fb368998b9703fa8c63db292a8ab9e988bf6da0c8a635754be8e69da1e7c1d \
--hash=sha256:467a4d730ae3bade87dba6bd769e837ab97e176968ce20591fe8f7bf819115b1 \
--hash=sha256:51b5166be86e09e011e92d9862b1fe64c4c7b9385f443fb535024e646d890460 \
--hash=sha256:53df7aea3cf301633cfa2b4b2c2d2441a87dfc878ef810e5b4eddcd3e68723ad \
--hash=sha256:54576669c1502b751b534bd76a4aeaaf838ed88b30af5d5c1b7d0a3ca5d4f7b5 \
--hash=sha256:54f0061028723c026020f5bb20649c22bc8a0d9f5363c283bdc5901d4d3bff01 \
--hash=sha256:58e3e21f6f13a7cca265cce492bc797425bd4cb2025fdd161a9e86a824ad65ce \
--hash=sha256:58ee34350f8c292dd24a050186c0e18301d80da904ef572cf5fda7be6a954929 \
--hash=sha256:5afc1fcf1830f9bb87d3b490ba03691081b9948a794ea851befd2643069a30c1 \
--hash=sha256:6541ffb70097885f7302cd73e2efd77be99841103023c2f9408551f27f45f7a5 \
--hash=sha256:666928ee735562a909d81bd2f63207b3214afd4ca41f790ab3025d066975c814 \
--hash=sha256:66be181965aff13301dd5f9b94b646ce39d99c7fe2fd5de1656f4ca7fafcb38c \
--hash=sha256:6b2ad5516f7068c7d9cbcda8ac5906c589e99bc427df2e1050282ee2d8bc2d58 \
--hash=sha256:73e14617a520c0f1bc15eb78c215383477e5ca70922ecaff1d29c63c060e04ca \
--hash=sha256:75d8a52bf8d1aa2ac968ae4b21b83b94fc7e5ea3dfbab34811fc60f32df505b2 \
--hash=sha256:76639dca5eb0afc6424ac5f42d43d3bd342ac710e06f38a8c877d5b96de09589 \
--hash=sha256:7cdf92116e9dfe40da17f921cdbfa0039dde9eb158914fa5f01b1e67a20b19cb \
--hash=sha256:7fb21e182dc6d83617e88dea002963d5cf99cf5eabbdbf04094f503d8fe8d723 \
--hash=sha256:84be69ea65f64fa01e5c4976be9826a5aa949f037508887add42da07420d65d6 \
--hash=sha256:8501d7875b176930e6ed9dbc1bc35adb37ef312f6106bd6bb5c204adb90160ac \
--hash=sha256:852b3f93c15fce58b8dc668bd54123713bfdbbb0796ba905ea5df99cfd083132 \
--hash=sha256:8658c1045766e87e0038323aa38b4a9f49b7f366563271f973c8890a98aa24b5 \
--hash=sha256:8f2df3968738a38d2a0058b5e721753f5d3d602346a1027b0dde31b0476418f3 \
--hash=sha256:8f808dcb0088a7a496cc9895e66a7b8de55ffea0eb9b547c75dfb216dd5f76ed \
--hash=sha256:908ff2de9c442b379143d1da3c886c63119d4eba22986806e2533cee603fe64b \
--hash=sha256:97b043fe8185ec53bb3ff0e59deb89425c0fc6ece6e118939963aab473505801 \
--hash=sha256:97f811ca7709c6ee8c0b55830f63b3d87086f4abbcbb189b4067e1cd7014db7b \
--hash=sha256:99c9fc5265566fb94731dc6826f43c5109e797078264e6389a36d47814473692 \
--hash=sha256:9cdbe8e80cc186d55f748a34393533a052d855357d5398a1ccb71a5021b58e8d \
--hash=sha256:9e9b395743e12c36a3167a3a9fd1b4e11d92fb0aa21ec98017ee6df639ed385e \
--hash=sha256:a42c7a8c62b29c4810e39da22b42524295fcb793f41c395c2cb07c126b729e83 \
--hash=sha256:a8162d81486de85ab1606e48e076431b66d44cf431b2b678e9cae458832e7147 \
--hash=sha256:abafeb82f85a651a9d6d642a33dc021606bc459c33e250925b25d6b9e7105a2e \
--hash=sha256:ada0d8d57e0f556ef38c24fee71bfe8d0db29c678bff2acd1819fc1b74f331c2 \
--hash=sha256:af5221e4f7800db3e84c46b79dba4112e3b3cc2678f808bdff4fcd2487073846 \
--hash=sha256:affb8fe36157c2dc8a7bc45b6a1875eb03e2c49167a1d52789144bdcb7ab3b8c \
--hash=sha256:b2e8b369f23f00678f6e673572209a5d3b0832f4991888e3df97af7b8b9decf3 \
--hash=sha256:b4e9ded8e80530bd7205a7a2b01802f934a4695ca9e9fbe1ce9644f5e0697864 \
--hash=sha256:b581107ec0c610cdea48b25f52030770be390db4a9a73ca58b8d70fa8a5ec32e \
--hash=sha256:b61f77d834f94b0099fa9ed35c189b7829759d4e9c2743697a130dd7ba62259f \
--hash=sha256:b685abb8b6d97989f6c69556d7934e0e533aa8822f50b9517ff2da06a1d29f23 \
--hash=sha256:b847a49377e64e92e11ef3d0a793de75451526c83af015bdafdd5d04de8a058a \
--hash=sha256:bf3093443751e5a419834162af358d1e31dec75f84747a91dbbc47b2c04fc085 \
--hash=sha256:bff7d3127ebc5cd908f3a72f6517f31f5247b84666137556a8fcc5177c560939 \
--hash=sha256:c04f9f1310ce414ab00bdcbf26d0906755094bfc59402cb66a7722c6f06d70b2 \
--hash=sha256:c1d33a622572d384f4c90b5f7a139328246ab5600141e90032b521c2127bd605 \
--hash=sha256:c29958265e4c2b937269e804b8a160c027ee1c2627d6152655008a8b8083630e \
--hash=sha256:c5075ce7b9286624cafcf36720ef1cfb2946d75430b87cb4d1f006e82cd71244 \
--hash=sha256:d05146497672f869baf41147d5ec1222788c70e5b8b0cfcd6e95597c75b5b96b \
--hash=sha256:d4b05a8f4ab7e7344459394094587b033fe259eea3a8720035e8ba30e79ab39b \
--hash=sha256:d55de67c48f06b7772541e8d4c062a2679205799ce904236e2836cb04c106442 \
--hash=sha256:db45028eae2fda7a24759c69ebeb2a7fbcc1a326606556448ed43ee480237a3c \
--hash=sha256:dd6384780c2a16097d47588844cd677316a90e0f41ef96ff485b62d58de79dcf \
--hash=sha256:de89585268ed8ee44e80126814cae63ff6b00d08416481f31b784570ef07ec59 \
--hash=sha256:df8fae2515a1e4936affccac3e7d506dd904de5ff82bc0b1433b4574a51b9bfb \
--hash=sha256:dfc63fabb7d8da8483ca836bae7e55766fe39c63253571e103c034ba8ea80950 \
--hash=sha256:e0f448b0eacbcc416feb634e1232a48d1cbde5e60f269c84e4fb0912f7bbb001 \
--hash=sha256:e3f2be79d4114d01f383096dbee51b57df141cb8b209c19d0cf65f23a24e75ba \
--hash=sha256:e414e1ca40386deda4291aa2d45062fea0fbaa14f95015738f8bb75c4d27f862 \
--hash=sha256:e5fd627e604ddc02db2ddb9ddc4a91dd92b7a6d6378fcf30bb37b49229072b89 \
--hash=sha256:f2059cd73b7ea779a9307d7a78ed743f0e3d33b88ccdcd84569abd2953cd859f \
--hash=sha256:f6da61cc38c1a95efc5edcedf258759e6dbab73191651a28c5719587f32a56ad \
--hash=sha256:f823fd1977071486739f484e27092765d693da6beedaceece54edce1dfeec9b2 \
--hash=sha256:fa4c0612893716bbb6595066ca9ecb517c982355abe39ba9d1f4ab834ace91ad \
--hash=sha256:fb379ac0ddfc86c5542a225d194f76ed468b071b6f79ff57c4b72e635605ad7d \
--hash=sha256:fdfdb3685b631d8efbb6d6d3d86eb631be2b408d9adafcadc11e63e3f9c96dec
requests-toolbelt==1.0.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \
--hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06
requests==2.31.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
rich==13.7.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa \
--hash=sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235
ruff==0.1.6 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc \
--hash=sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e \
--hash=sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6 \
--hash=sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a \
--hash=sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184 \
--hash=sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76 \
--hash=sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745 \
--hash=sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33 \
--hash=sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543 \
--hash=sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248 \
--hash=sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240 \
--hash=sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc \
--hash=sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703 \
--hash=sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35 \
--hash=sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff \
--hash=sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462 \
--hash=sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc
secretstorage==3.3.3 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "linux" \
--hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \
--hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99
shellingham==1.5.4 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \
--hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de
sniffio==1.3.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \
--hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384
structlog==23.2.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:16a167e87b9fa7fae9a972d5d12805ef90e04857a93eba479d4be3801a6a1482 \
--hash=sha256:334666b94707f89dbc4c81a22a8ccd34449f0201d5b1ee097a030b577fa8c858
tomlkit==0.12.3 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4 \
--hash=sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba
trove-classifiers==2023.11.22 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:533df77e284fd645d90deeafd3ef710d290884efafe4f5009aa1663f95aec992 \
--hash=sha256:c31a7e92f965f060a244b57d8ed5ee6f53fcb413ee17ce790e00577cb369ad99
urllib3==2.1.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3 \
--hash=sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54
virtualenv==20.24.7 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:69050ffb42419c91f6c1284a7b24e0475d793447e35929b488bf6a0aade39353 \
--hash=sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd
xattr==0.10.1 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "darwin" \
--hash=sha256:042ad818cda6013162c0bfd3816f6b74b7700e73c908cde6768da824686885f8 \
--hash=sha256:0aedf55b116beb6427e6f7958ccd80a8cbc80e82f87a4cd975ccb61a8d27b2ee \
--hash=sha256:0e14bd5965d3db173d6983abdc1241c22219385c22df8b0eb8f1846c15ce1fee \
--hash=sha256:13279fe8f7982e3cdb0e088d5cb340ce9cbe5ef92504b1fd80a0d3591d662f68 \
--hash=sha256:148466e5bb168aba98f80850cf976e931469a3c6eb11e9880d9f6f8b1e66bd06 \
--hash=sha256:16a660a883e703b311d1bbbcafc74fa877585ec081cd96e8dd9302c028408ab1 \
--hash=sha256:183ad611a2d70b5a3f5f7aadef0fcef604ea33dcf508228765fd4ddac2c7321d \
--hash=sha256:199b20301b6acc9022661412346714ce764d322068ef387c4de38062474db76c \
--hash=sha256:1dc9b9f580ef4b8ac5e2c04c16b4d5086a611889ac14ecb2e7e87170623a0b75 \
--hash=sha256:1e2973e72faa87ca29d61c23b58c3c89fe102d1b68e091848b0e21a104123503 \
--hash=sha256:1f0563196ee54756fe2047627d316977dc77d11acd7a07970336e1a711e934db \
--hash=sha256:209fb84c09b41c2e4cf16dd2f481bb4a6e2e81f659a47a60091b9bcb2e388840 \
--hash=sha256:2677d40b95636f3482bdaf64ed9138fb4d8376fb7933f434614744780e46e42d \
--hash=sha256:295b3ab335fcd06ca0a9114439b34120968732e3f5e9d16f456d5ec4fa47a0a2 \
--hash=sha256:3725746a6502f40f72ef27e0c7bfc31052a239503ff3eefa807d6b02a249be22 \
--hash=sha256:3e5825b5fc99ecdd493b0cc09ec35391e7a451394fdf623a88b24726011c950d \
--hash=sha256:3e739d624491267ec5bb740f4eada93491de429d38d2fcdfb97b25efe1288eca \
--hash=sha256:3ff0dbe4a6ce2ce065c6de08f415bcb270ecfd7bf1655a633ddeac695ce8b250 \
--hash=sha256:40039f1532c4456fd0f4c54e9d4e01eb8201248c321c6c6856262d87e9a99593 \
--hash=sha256:436e1aaf23c07e15bed63115f1712d2097e207214fc6bcde147c1efede37e2c5 \
--hash=sha256:46c32cd605673606b9388a313b0050ee7877a0640d7561eea243ace4fa2cc5a6 \
--hash=sha256:475c38da0d3614cc5564467c4efece1e38bd0705a4dbecf8deeb0564a86fb010 \
--hash=sha256:485539262c2b1f5acd6b6ea56e0da2bc281a51f74335c351ea609c23d82c9a79 \
--hash=sha256:49626096ddd72dcc1654aadd84b103577d8424f26524a48d199847b5d55612d0 \
--hash=sha256:4abef557028c551d59cf2fb3bf63f2a0c89f00d77e54c1c15282ecdd56943496 \
--hash=sha256:5267e5f9435c840d2674194150b511bef929fa7d3bc942a4a75b9eddef18d8d8 \
--hash=sha256:5b49d591cf34cda2079fd7a5cb2a7a1519f54dc2e62abe3e0720036f6ed41a85 \
--hash=sha256:5bc40570155beb85e963ae45300a530223d9822edfdf09991b880e69625ba38a \
--hash=sha256:5dc6099e76e33fa3082a905fe59df766b196534c705cf7a2e3ad9bed2b8a180e \
--hash=sha256:636ebdde0277bce4d12d2ef2550885804834418fee0eb456b69be928e604ecc4 \
--hash=sha256:6b8705ac6791426559c1a5c2b88bb2f0e83dc5616a09b4500899bfff6a929302 \
--hash=sha256:6b905e808df61b677eb972f915f8a751960284358b520d0601c8cbc476ba2df6 \
--hash=sha256:7298455ccf3a922d403339781b10299b858bb5ec76435445f2da46fb768e31a5 \
--hash=sha256:772b22c4ff791fe5816a7c2a1c9fcba83f9ab9bea138eb44d4d70f34676232b4 \
--hash=sha256:7880c8a54c18bc091a4ce0adc5c6d81da1c748aec2fe7ac586d204d6ec7eca5b \
--hash=sha256:789bd406d1aad6735e97b20c6d6a1701e1c0661136be9be862e6a04564da771f \
--hash=sha256:7f9be588a4b6043b03777d50654c6079af3da60cc37527dbb80d36ec98842b1e \
--hash=sha256:80638d1ce7189dc52f26c234cee3522f060fadab6a8bc3562fe0ddcbe11ba5a4 \
--hash=sha256:8068df3ebdfa9411e58d5ae4a05d807ec5994645bb01af66ec9f6da718b65c5b \
--hash=sha256:827b5a97673b9997067fde383a7f7dc67342403093b94ea3c24ae0f4f1fec649 \
--hash=sha256:89c93b42c3ba8aedbc29da759f152731196c2492a2154371c0aae3ef8ba8301b \
--hash=sha256:8faaacf311e2b5cc67c030c999167a78a9906073e6abf08eaa8cf05b0416515c \
--hash=sha256:925284a4a28e369459b2b7481ea22840eed3e0573a4a4c06b6b0614ecd27d0a7 \
--hash=sha256:986c2305c6c1a08f78611eb38ef9f1f47682774ce954efb5a4f3715e8da00d5f \
--hash=sha256:9d4c306828a45b41b76ca17adc26ac3dc00a80e01a5ba85d71df2a3e948828f2 \
--hash=sha256:a126eb38e14a2f273d584a692fe36cff760395bf7fc061ef059224efdb4eb62c \
--hash=sha256:a3878e1aff8eca64badad8f6d896cb98c52984b1e9cd9668a3ab70294d1ef92d \
--hash=sha256:a5ea974930e876bc5c146f54ac0f85bb39b7b5de2b6fc63f90364712ae368ebe \
--hash=sha256:a606280b0c9071ef52572434ecd3648407b20df3d27af02c6592e84486b05894 \
--hash=sha256:a9a7a807ab538210ff8532220d8fc5e2d51c212681f63dbd4e7ede32543b070f \
--hash=sha256:aa32f1b45fed9122bed911de0fcc654da349e1f04fa4a9c8ef9b53e1cc98b91e \
--hash=sha256:b0e919c24f5b74428afa91507b15e7d2ef63aba98e704ad13d33bed1288dca81 \
--hash=sha256:b27dfc13b193cb290d5d9e62f806bb9a99b00cd73bb6370d556116ad7bb5dc12 \
--hash=sha256:b34df5aad035d0343bd740a95ca30db99b776e2630dca9cc1ba8e682c9cc25ea \
--hash=sha256:b7bc4ae264aa679aacf964abf3ea88e147eb4a22aea6af8c6d03ebdebd64cfd6 \
--hash=sha256:c0cd2d02ef2fb45ecf2b0da066a58472d54682c6d4f0452dfe7ae2f3a76a42ea \
--hash=sha256:c12e7d81ffaa0605b3ac8c22c2994a8e18a9cf1c59287a1b7722a2289c952ec5 \
--hash=sha256:c3024a9ff157247c8190dd0eb54db4a64277f21361b2f756319d9d3cf20e475f \
--hash=sha256:c4120090dac33eddffc27e487f9c8f16b29ff3f3f8bcb2251b2c6c3f974ca1e1 \
--hash=sha256:c5d3d0e728bace64b74c475eb4da6148cd172b2d23021a1dcd055d92f17619ac \
--hash=sha256:cc6b8d5ca452674e1a96e246a3d2db5f477aecbc7c945c73f890f56323e75203 \
--hash=sha256:ceaa26bef8fcb17eb59d92a7481c2d15d20211e217772fb43c08c859b01afc6a \
--hash=sha256:d1ef954d0655f93a34d07d0cc7e02765ec779ff0b59dc898ee08c6326ad614d5 \
--hash=sha256:d60c27922ec80310b45574351f71e0dd3a139c5295e8f8b19d19c0010196544f \
--hash=sha256:e31d062cfe1aaeab6ba3db6bd255f012d105271018e647645941d6609376af18 \
--hash=sha256:e8c014c371391f28f8cd27d73ea59f42b30772cd640b5a2538ad4f440fd9190b \
--hash=sha256:ec0956a8ab0f0d3f9011ba480f1e1271b703d11542375ef73eb8695a6bd4b78b \
--hash=sha256:f1be6e733e9698f645dbb98565bb8df9b75e80e15a21eb52787d7d96800e823b \
--hash=sha256:f24a7c04ff666d0fe905dfee0a84bc899d624aeb6dccd1ea86b5c347f15c20c1 \
--hash=sha256:f55a2dd73a12a1ae5113c5d9cd4b4ab6bf7950f4d76d0a1a0c0c4264d50da61d \
--hash=sha256:fc354f086f926a1c7f04886f97880fed1a26d20e3bc338d0d965fd161dbdb8ab \
--hash=sha256:ffcb57ca1be338d69edad93cf59aac7c6bb4dbb92fd7bf8d456c69ea42f7e6d2
zipp==3.17.0 ; python_version >= "3.11" and python_version < "3.12" \
--hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \
--hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0
================================================
FILE: requirements.txt
================================================
dnslib==0.9.23 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:310196d3e38ce2051b61eebbd2f1d08fcc934fa3360f22031864d16efe8bca77 \
--hash=sha256:46137e8ef6ef52b24a16d47e0786a99dd103ab1e71eea616f21371accbccc557 \
--hash=sha256:9eb851ac721eea51834d43795478ac9b48272c61ba97cc4a160668b50aff39ec
markdown-it-py==3.0.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
--hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
mdurl==0.1.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
pygments==2.17.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367
rich==13.7.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa \
--hash=sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235
structlog==23.2.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:16a167e87b9fa7fae9a972d5d12805ef90e04857a93eba479d4be3801a6a1482 \
--hash=sha256:334666b94707f89dbc4c81a22a8ccd34449f0201d5b1ee097a030b577fa8c858
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/conftest.py
================================================
import pytest
import pytest_asyncio
import logging
import asyncio
import contextlib
import random
import string
import dns.asyncresolver
from dnschef import kitchen
from dnschef.api import app
from dnschef.protocols import start_server
from dnschef.utils import parse_config_file
from dnschef.logger import log, json_capture_formatter #,debug_formatter
from fastapi.testclient import TestClient
#log.setLevel(logging.DEBUG)
#log.handlers[0].setFormatter(debug_formatter)
jh = logging.StreamHandler()
jh.setFormatter(json_capture_formatter)
log.addHandler(jh)
@pytest.fixture
def random_string():
return ''.join(random.choices(string.ascii_letters, k=6))
@pytest.fixture
def random_string_gen():
def _random_gen():
while True:
yield ''.join(random.choices(string.ascii_letters, k=6))
return _random_gen()
@pytest.fixture
def api_test_client():
return TestClient(app)
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest_asyncio.fixture(scope="session")
async def dns_client():
resolver = dns.asyncresolver.Resolver()
resolver.nameservers = ['127.0.0.1']
resolver.port = 5553
yield resolver
@pytest.fixture(scope="session")
def config_file():
return parse_config_file("tests/dnschef-tests.toml")
@pytest_asyncio.fixture(scope="session", autouse=True)
async def start_dnschef(config_file):
kitchen.CONFIG = config_file
server_task = asyncio.create_task(
start_server(
interface="127.0.0.1",
nameservers=["8.8.8.8"],
tcp=True,
ipv6=False,
port=5553
))
yield
server_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await server_task
================================================
FILE: tests/dnschef-tests.toml
================================================
[A] # Queries for IPv4 address records
"*.thesprawl.org" = "100.100.100.100"
"*.test.thesprawl.org" = "127.0.0.1"
"*.*.thesprawl.org" = "1.1.1.1"
"c.*.*.thesprawl.org" = "1.1.2.2"
"fuck.shit.com" = "192.168.0.1"
"*.wat.org" = { file = "tests/small-bin-test", chunk_size = 122 }
[AAAA] # Queries for IPv6 address records
"*.thesprawl.org" = "2001:db8::1"
"*.wat.org" = { file = "tests/small-bin-test", chunk_size = 122 }
[MX] # Queries for mail server records
"*.thesprawl.org" = "mail.fake.com"
[NS] # Queries for mail server records
"*.thesprawl.org" = "ns.fake.com"
[CNAME] # Queries for alias records
"*.thesprawl.org" = "www.fake.com"
[TXT] # Queries for text records
"*.thesprawl.org" = "fake message"
"ok.thesprawl.org" = "fake message"
"*.something.wattahog.org" = "fuck off"
"wa*.aint.nothing.org" = "sequoia banshee boogers"
"ns*.shit.fuck.org" = { file = "tests/thicc-bin-test", chunk_size = 189, response_format = "{prefix}test-{chunk}", response_prefix_pool = ["atlassian-domain-verification=", "onetrust-domain-verification=", "docusign=" ] }
"ns*.fronted.brick.org" = { file = "tests/thicc-bin-test" }
"ns*.filtered.crack.org" = { file = "tests/thicc-bin-test", chunk_size = 50, response_format = "{prefix}test-{chunk}", response_prefix_pool = ["atlassian-domain-verification=", "onetrust-domain-verification=", "docusign=" ] }
[TXT."*.wattahog.org"]
file = "tests/thicc-bin-test"
chunk_size = 189
response_format = "{prefix}test-{chunk}"
response_prefix_pool = [ "atlassian-domain-verification=", "onetrust-domain-verification=" , "docusign=" ]
[PTR]
"*.2.0.192.in-addr.arpa" = "fake.com"
"*.thesprawl.org" = "fake.com"
[SOA]
# FORMAT: mname rname t1 t2 t3 t4 t5
"*.thesprawl.org" = "ns.fake.com. hostmaster.fake.com. 1 10800 3600 604800 3600"
[NAPTR]
# FORMAT: order preference flags service regexp replacement
"*.thesprawl.org" = "100 10 U E2U+sip !^.*$!sip:customer-service@fake.com! ."
[SRV]
# FORMAT: priority weight port target
"*.thesprawl.org" = "0 5 5060 sipserver.fake.com"
[DNSKEY]
# FORMAT: flags protocol algorithm base64(key)
"*.thesprawl.org" = "256 3 5 AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQeogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU/TpPSEDhm2SNKLijfUppn1UaNvv4w=="
[RRSIG]
# FORMAT: covered algorithm labels labels orig_ttl sig_exp sig_inc key_tag name base64(sig)
"*.thesprawl.org" = "A 5 3 86400 20030322173103 20030220173103 2642 thesprawl.org. oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTrPYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6oB9wfuh3DTJXUAfI/M0zmO/zz8bW0Rznl8O3tGNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkGJ5D6fwFm8nN+6pBzeDQfsS3Ap3o="
[HTTPS]
# FORMAT: priority target key=value pairs
"*.thesprawl.org" = "1 . alpn=h2 ipv4hint=127.0.0.1 ipv6hint=::1"
================================================
FILE: tests/small-bin-test
================================================
#!/bin/sh
cmd=${0##*/}
exec grep -F "$@"
================================================
FILE: tests/test_dns_server.py
================================================
import pytest
import difflib
from dnslib import RDMAP
@pytest.mark.asyncio
async def test_proxy_request(dns_client):
for proto in [False, True]:
await dns_client.resolve("google.com", "A", tcp=proto)
@pytest.mark.asyncio
async def test_fake_A_response(dns_client):
for proto in [False, True]:
answers = await dns_client.resolve("fuck.shit.com", "A", tcp=proto)
assert answers[0].address == "192.168.0.1"
@pytest.mark.asyncio
async def test_correct_wildcard_behavior(dns_client):
for proto in [False, True]:
answers = await dns_client.resolve("thesprawl.org", "A", tcp=proto, raise_on_no_answer = False)
assert not len(answers)
answers = await dns_client.resolve("test.thesprawl.org", "A", tcp=proto)
assert answers[0].address == "100.100.100.100"
answers = await dns_client.resolve("err.thesprawl.org", "A", tcp=proto)
assert answers[0].address == "100.100.100.100"
answers = await dns_client.resolve("ok.test.thesprawl.org", "A", tcp=proto)
assert answers[0].address == "127.0.0.1"
answers = await dns_client.resolve("not.bad.thesprawl.org", "A", tcp=proto)
assert answers[0].address == "1.1.1.1"
answers = await dns_client.resolve("c.bad.wat.thesprawl.org", "A", tcp=proto)
assert answers[0].address == "1.1.2.2"
answers = await dns_client.resolve("wa1.aint.nothing.org", "TXT", tcp=proto)
assert answers[0].to_text().strip('"') == 'sequoia banshee boogers'
answers = await dns_client.resolve("wattahog.aint.nothing.org", "TXT", tcp=proto)
assert answers[0].to_text().strip('"') == 'sequoia banshee boogers'
@pytest.mark.asyncio
async def test_fake_wildcard_records(dns_client, random_string, config_file):
for proto in [False, True]:
for record in RDMAP.keys():
if record == "RRSIG" or record not in config_file:
continue
answers = await dns_client.resolve(
f"{random_string}.thesprawl.org",
record,
tcp=proto
)
#assert answers[0].to_text().replace('"', '').rstrip('.') == config_file[record]["*.thesprawl.org"]
assert difflib.SequenceMatcher(
a=answers[0].to_text(),
b=config_file[record]["*.thesprawl.org"]
).quick_ratio() > 0.86
================================================
FILE: tests/test_file_staging.py
================================================
import pytest
import hashlib
from base64 import b64decode
from ipaddress import IPv4Address, IPv6Address
def compare_file_digests(tmp_file_path, orig_file_path):
with tmp_file_path.open('rb') as staged_file:
with open(orig_file_path, 'rb') as orig_file:
staged_file_digest = hashlib.file_digest(staged_file, "md5").digest()
orig_file_digest = hashlib.file_digest(orig_file, "md5").digest()
return staged_file_digest == orig_file_digest
@pytest.mark.asyncio
async def test_A_file_staging(dns_client, tmp_path, random_string_gen):
orig_file_path = "tests/small-bin-test"
for proto in [False, True]:
chunk_n = 0
tmp_file_path = tmp_path / next(random_string_gen)
with tmp_file_path.open('ab') as f:
while True:
answers = await dns_client.resolve(f"lala{chunk_n}dayum.wat.org", "A", tcp=proto, raise_on_no_answer=False)
print(list(answers))
for answer in answers:
data = IPv4Address(answer.address).packed
data = data.replace(b'\x00', b'')
f.write(data)
if not len(answers):
break
chunk_n += 1
assert compare_file_digests(tmp_file_path, orig_file_path) == True
@pytest.mark.asyncio
async def test_AAAA_file_staging(dns_client, tmp_path, random_string_gen):
orig_file_path = "tests/small-bin-test"
for proto in [False, True]:
chunk_n = 0
tmp_file_path = tmp_path / next(random_string_gen)
with tmp_file_path.open('ab') as f:
while True:
answers = await dns_client.resolve(f"lala{chunk_n}dayum.wat.org", "AAAA", tcp=proto, raise_on_no_answer=False)
for answer in answers:
data = IPv6Address(answer.address).packed
data = data.replace(b'\x00', b'')
f.write(data)
if not len(answers):
break
chunk_n += 1
assert compare_file_digests(tmp_file_path, orig_file_path) == True
@pytest.mark.asyncio
async def test_TXT_file_staging(dns_client, tmp_path, random_string_gen):
orig_file_path = "tests/thicc-bin-test"
for proto in [False, True]:
chunk_n = 0
tmp_file_path = tmp_path / next(random_string_gen)
with tmp_file_path.open('ab') as f:
while True:
answers = await dns_client.resolve(f"ns{chunk_n}.fronted.brick.org", "TXT", tcp=proto, raise_on_no_answer=False)
for answer in answers:
f.write(b64decode(answer.to_text().strip('"')))
if not len(answers):
break
chunk_n += 1
assert compare_file_digests(tmp_file_path, orig_file_path) == True
================================================
FILE: tests/test_http_api.py
================================================
import json
def test_get_records(api_test_client, config_file):
r = api_test_client.get("/")
assert r.status_code == 200
assert r.json() == config_file
def test_add_record(api_test_client):
r = api_test_client.put(
"/",
json={"type": "A", "domain": "*.nashvillenibblers.com", "value": "192.168.69.69"}
)
assert r.status_code == 200
r = api_test_client.get("/")
assert r.status_code == 200
assert r.json()["A"]["*.nashvillenibblers.com"] == "192.168.69.69"
def test_delete_record(api_test_client):
r = api_test_client.request(
method="DELETE",
url="/",
content=json.dumps({"type": "A", "domain": "*.nashvillenibblers.com", "value": "192.168.69.69"}).encode()
)
assert r.status_code == 200
r = api_test_client.get("/")
assert r.status_code == 200
assert not r.json()["A"].get("*.nashvillenibblers.com", None)
def test_logs(api_test_client):
r = api_test_client.get("/logs")
assert r.status_code == 200
r = api_test_client.get(
"/logs",
params={"type": "A"}
)
assert r.status_code == 200
assert len(r.json())
r = api_test_client.get(
"/logs",
params={"name": "fuck.shit.com"}
)
assert r.status_code == 200
assert len(r.json())
r = api_test_client.get(
"/logs",
params={"name": "fuck.shit.com", "type": "A"}
)
assert r.status_code == 200
assert len(r.json())
================================================
FILE: tests/test_util.py
================================================
import pytest
from dnschef import __version__
from dnschef.utils import header
@pytest.mark.asyncio
async def test_config_parse(config_file):
assert len(config_file)
@pytest.mark.asyncio
async def test_header():
assert __version__ in header
gitextract_1kroxokl/
├── .devcontainer/
│ └── devcontainer.json
├── .dockerignore
├── .github/
│ └── workflows/
│ ├── python-package.yml
│ ├── python-publish-test.yml
│ └── python-publish.yml
├── .gitignore
├── CHANGELOG
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── TODO
├── dnschef/
│ ├── __init__.py
│ ├── __main__.py
│ ├── api.py
│ ├── kitchen.py
│ ├── logger.py
│ ├── protocols.py
│ └── utils.py
├── dnschef.toml
├── docker-compose.yml
├── pyproject.toml
├── requirements-api.txt
├── requirements-dev.txt
├── requirements.txt
└── tests/
├── __init__.py
├── conftest.py
├── dnschef-tests.toml
├── small-bin-test
├── test_dns_server.py
├── test_file_staging.py
├── test_http_api.py
├── test_util.py
└── thicc-bin-test
SYMBOL INDEX (69 symbols across 10 files)
FILE: dnschef/__main__.py
function main (line 48) | def main():
FILE: dnschef/api.py
class Record (line 26) | class Record(BaseModel):
class Settings (line 31) | class Settings(BaseSettings):
function startup_event (line 46) | async def startup_event():
function add_record (line 79) | async def add_record(record: Record):
function delete_record (line 85) | async def delete_record(record: Record):
function get_records (line 91) | async def get_records():
function get_logs (line 96) | async def get_logs(type: Optional[DnsQueryType] = None, name: Optional[s...
FILE: dnschef/kitchen.py
function chunk_string (line 18) | def chunk_string(string_to_chunk: str, chunk_size: int):
function chunk_file (line 27) | def chunk_file(file_path: pathlib.Path, chunk_size: int):
function get_file_chunk (line 36) | def get_file_chunk(file_path, chunk_index, chunk_size):
function stage_file (line 44) | async def stage_file(qname, record, chunk_size: int):
class DNSKitchen (line 55) | class DNSKitchen:
method do_default (line 57) | async def do_default(self, addr, qname, qtype, record):
method do_A (line 61) | async def do_A(self, addr, qname, qtype, record):
method do_TXT (line 76) | async def do_TXT(self, addr, qname, qtype, record):
method do_AAAA (line 99) | async def do_AAAA(self, addr, qname, qtype, record):
method do_HTTPS (line 114) | async def do_HTTPS(self, addr, qname, qtype, record):
method do_SOA (line 119) | async def do_SOA(self, addr, qname, qtype, record):
method do_NAPTR (line 129) | async def do_NAPTR(self, addr, qname, qtype, record):
method do_SRV (line 139) | async def do_SRV(self, addr, qname, qtype, record):
method do_DNSKEY (line 148) | async def do_DNSKEY(self, addr, qname, qtype, record):
method do_RRSIG (line 157) | async def do_RRSIG(self, addr, qname, qtype, record):
method findnametodns (line 173) | def findnametodns(self, qname, qtype):
method we_cookin (line 192) | async def we_cookin(self, logger, d, qtype, qname, addr):
FILE: dnschef/protocols.py
class ClientProtocol (line 13) | class ClientProtocol(enum.Enum):
class UdpDnsClientProtocol (line 17) | class UdpDnsClientProtocol:
method __init__ (line 18) | def __init__(self, request, on_con_lost):
method connection_made (line 23) | def connection_made(self, transport):
method datagram_received (line 28) | def datagram_received(self, data, addr):
method error_received (line 33) | def error_received(self, exc):
method connection_lost (line 36) | def connection_lost(self, exc):
class TcpDnsClientProtocol (line 40) | class TcpDnsClientProtocol(asyncio.Protocol):
method __init__ (line 41) | def __init__(self, request, on_con_lost):
method connection_made (line 45) | def connection_made(self, transport):
method data_received (line 50) | def data_received(self, data):
method connection_lost (line 56) | def connection_lost(self, exc):
function proxy_request (line 62) | async def proxy_request(request, host, protocol: ClientProtocol, port: i...
class UdpDnsServerProtocol (line 83) | class UdpDnsServerProtocol:
method __init__ (line 84) | def __init__(self, nameservers, dns_kitchen):
method connection_made (line 88) | def connection_made(self, transport):
method datagram_received (line 91) | def datagram_received(self, data, addr):
class TcpDnsServerProtocol (line 129) | class TcpDnsServerProtocol(asyncio.Protocol):
method __init__ (line 130) | def __init__(self, nameservers, dns_kitchen):
method connection_made (line 134) | def connection_made(self, transport):
method data_received (line 137) | def data_received(self, data):
function start_server (line 178) | async def start_server(interface: str, nameservers: List[str], tcp: bool...
FILE: dnschef/utils.py
function parse_config_file (line 15) | def parse_config_file(config_file: str = "dnschef.toml"):
FILE: tests/conftest.py
function random_string (line 25) | def random_string():
function random_string_gen (line 29) | def random_string_gen():
function api_test_client (line 37) | def api_test_client():
function event_loop (line 41) | def event_loop():
function dns_client (line 47) | async def dns_client():
function config_file (line 54) | def config_file():
function start_dnschef (line 58) | async def start_dnschef(config_file):
FILE: tests/test_dns_server.py
function test_proxy_request (line 6) | async def test_proxy_request(dns_client):
function test_fake_A_response (line 11) | async def test_fake_A_response(dns_client):
function test_correct_wildcard_behavior (line 17) | async def test_correct_wildcard_behavior(dns_client):
function test_fake_wildcard_records (line 45) | async def test_fake_wildcard_records(dns_client, random_string, config_f...
FILE: tests/test_file_staging.py
function compare_file_digests (line 7) | def compare_file_digests(tmp_file_path, orig_file_path):
function test_A_file_staging (line 16) | async def test_A_file_staging(dns_client, tmp_path, random_string_gen):
function test_AAAA_file_staging (line 38) | async def test_AAAA_file_staging(dns_client, tmp_path, random_string_gen):
function test_TXT_file_staging (line 59) | async def test_TXT_file_staging(dns_client, tmp_path, random_string_gen):
FILE: tests/test_http_api.py
function test_get_records (line 3) | def test_get_records(api_test_client, config_file):
function test_add_record (line 8) | def test_add_record(api_test_client):
function test_delete_record (line 19) | def test_delete_record(api_test_client):
function test_logs (line 31) | def test_logs(api_test_client):
FILE: tests/test_util.py
function test_config_parse (line 6) | async def test_config_parse(config_file):
function test_header (line 10) | async def test_header():
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (164K chars).
[
{
"path": ".devcontainer/devcontainer.json",
"chars": 1074,
"preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.co"
},
{
"path": ".dockerignore",
"chars": 23,
"preview": "tests\n__pycache__\n*.pyc"
},
{
"path": ".github/workflows/python-package.yml",
"chars": 1308,
"preview": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more inform"
},
{
"path": ".github/workflows/python-publish-test.yml",
"chars": 714,
"preview": "name: Upload Package to PyPi Testing\n\non:\n workflow_dispatch:\n #release:\n # types: [published]\n\npermissions:\n conte"
},
{
"path": ".github/workflows/python-publish.yml",
"chars": 606,
"preview": "name: Upload Package to PyPi\n\non:\n release:\n types: [published]\n workflow_dispatch:\n\npermissions:\n contents: read\n"
},
{
"path": ".gitignore",
"chars": 2070,
"preview": ".vscode\n.DS_Store\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n*.log\n.ruff_cache\n\n# C exten"
},
{
"path": "CHANGELOG",
"chars": 1304,
"preview": "Version 0.5\n\n* Complete re-write, now fully asynchronous (uses Python's AsyncIO library)\n\nVersion 0.4\n\n* Ported to Pytho"
},
{
"path": "Dockerfile",
"chars": 466,
"preview": "FROM python:3.11-slim as build-stage\n\nWORKDIR /tmp/code\n\nCOPY . .\n\nRUN pip wheel --wheel-dir ./dist '.[api]'\n\nFROM pytho"
},
{
"path": "LICENSE",
"chars": 1521,
"preview": "Copyright (C) 2014 Peter Kacherginsky, Marcello Salvati\nAll rights reserved.\n\nRedistribution and use in source and binar"
},
{
"path": "Makefile",
"chars": 595,
"preview": ".PHONY: tests\n\ndefault: build\n\nclean:\n\trm -f -r build/\n\trm -f -r bin/\n\trm -f -r dist/\n\trm -f -r *.egg-info\n\tfind . -name"
},
{
"path": "README.md",
"chars": 26241,
"preview": "> [!NOTE]\n> This is an updated version of [DNSChef](https://github.com/iphelix/dnschef) originally written by [@iphelix]"
},
{
"path": "TODO",
"chars": 52,
"preview": "[*] Run in MiTM mode and inject fake DNS responses.\n"
},
{
"path": "dnschef/__init__.py",
"chars": 82,
"preview": "import importlib.metadata\n\n__version__ = importlib.metadata.version(\"dnschef-ng\")\n"
},
{
"path": "dnschef/__main__.py",
"chars": 12105,
"preview": "#!/usr/bin/env python3\n\n#\n# DNSChef is a highly configurable DNS Proxy for Penetration Testers \n# and Malware Analysts. "
},
{
"path": "dnschef/api.py",
"chars": 2839,
"preview": "from dnschef import __version__\nfrom dnschef import kitchen\nfrom dnschef.protocols import start_server\nfrom dnschef.util"
},
{
"path": "dnschef/kitchen.py",
"chars": 8080,
"preview": "from dnslib import *\nfrom ipaddress import IPv4Address, IPv6Address\n\nfrom dnschef.logger import log\n\nimport difflib\nimpo"
},
{
"path": "dnschef/logger.py",
"chars": 2148,
"preview": "from rich.traceback import install\n\nimport logging\nimport logging.handlers\nimport structlog\n\ninstall(show_locals=True)\n\n"
},
{
"path": "dnschef/protocols.py",
"chars": 6844,
"preview": "import asyncio\nimport socket\nimport re\nimport random\nimport functools\nimport enum\nfrom dnslib import DNSRecord, QR, QTYP"
},
{
"path": "dnschef/utils.py",
"chars": 1230,
"preview": "import tomllib\nfrom dnschef import __version__\nfrom dnschef.logger import log\nfrom dnslib import RDMAP\n\nheader = \" "
},
{
"path": "dnschef.toml",
"chars": 2384,
"preview": "[A] # Queries for IPv4 address records\n\"*.thesprawl.org\" = \"100.100.100.100\"\n\"*.test.thesprawl.org\" = \"127.0.0.1\"\n\"*.*"
},
{
"path": "docker-compose.yml",
"chars": 378,
"preview": "version: \"3\"\nservices:\n dnschef:\n image: dnschef:latest\n container_name: dnschef\n ports:\n - \"53:53/udp\"\n "
},
{
"path": "pyproject.toml",
"chars": 1527,
"preview": "[tool.poetry]\nname = \"dnschef-ng\"\nversion = \"0.7.2\"\ndescription = \"A highly configurable DNS proxy for Penetration Teste"
},
{
"path": "requirements-api.txt",
"chars": 13937,
"preview": "annotated-types==0.6.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n --hash=sha256:0641064de18ba7a25dee8f9"
},
{
"path": "requirements-dev.txt",
"chars": 57035,
"preview": "anyio==3.7.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n --hash=sha256:44a3c9aba0f5defa43261a8b3efb97891"
},
{
"path": "requirements.txt",
"chars": 1527,
"preview": "dnslib==0.9.23 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n --hash=sha256:310196d3e38ce2051b61eebbd2f1d08"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/conftest.py",
"chars": 1812,
"preview": "import pytest\nimport pytest_asyncio\nimport logging\nimport asyncio\nimport contextlib\nimport random\nimport string\nimport d"
},
{
"path": "tests/dnschef-tests.toml",
"chars": 2758,
"preview": "[A] # Queries for IPv4 address records\n\"*.thesprawl.org\" = \"100.100.100.100\"\n\"*.test.thesprawl.org\" = \"127.0.0.1\"\n\"*.*"
},
{
"path": "tests/small-bin-test",
"chars": 41,
"preview": "#!/bin/sh\ncmd=${0##*/}\nexec grep -F \"$@\"\n"
},
{
"path": "tests/test_dns_server.py",
"chars": 2394,
"preview": "import pytest\nimport difflib\nfrom dnslib import RDMAP\n\n@pytest.mark.asyncio\nasync def test_proxy_request(dns_client):\n "
},
{
"path": "tests/test_file_staging.py",
"chars": 2858,
"preview": "import pytest\nimport hashlib\nfrom base64 import b64decode\nfrom ipaddress import IPv4Address, IPv6Address\n\n\ndef compare_f"
},
{
"path": "tests/test_http_api.py",
"chars": 1468,
"preview": "import json\n\ndef test_get_records(api_test_client, config_file):\n r = api_test_client.get(\"/\")\n assert r.status_co"
},
{
"path": "tests/test_util.py",
"chars": 251,
"preview": "import pytest\nfrom dnschef import __version__\nfrom dnschef.utils import header\n\n@pytest.mark.asyncio\nasync def test_conf"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the byt3bl33d3r/dnschef-ng GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (154.0 KB), approximately 60.1k tokens, and a symbol index with 69 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.