diff --git a/.example.env b/.example.env new file mode 100644 index 0000000..f09b74c --- /dev/null +++ b/.example.env @@ -0,0 +1,8 @@ +# Crawler Settings +CRAWLER_CIDR=0.0.0.0/0 # Warning: This is a broad range; adjust as needed +CRAWLER_DISCOVERY_TOP_PORTS=1000 # Options: number (e.g., 100, 1000) or 'full' +CRAWLER_FINGERPRINT_FAST_MODE=false # true to only scan services default ports + +# MAXMIND for GeoIP and ASN lookups +MAXMIND_ACCOUNT_ID=your_user_id_here +MAXMIND_LICENSE_KEY=your_license_key_here diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f87ff7e..0b4f9de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,28 +1,92 @@ -name: pre-commit checks +name: Go Tests on: - pull_request: push: - branches: - - main + branches: [ main, develop ] + paths: + - 'rigour/**' + - '.github/workflows/go-tests.yml' + pull_request: + branches: [ main, develop ] + paths: + - 'rigour/**' jobs: - pre-commit: + test: + name: Test Go Services runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.25.5'] + steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache-dependency-path: rigour/go.sum + + - name: Install libpcap-dev + run: | + sudo apt-get update + sudo apt-get install -y libpcap-dev + + - name: Verify dependencies + working-directory: ./rigour + run: | + go mod verify + go mod download + + - name: Run go vet + working-directory: ./rigour + run: | + # Exclude third_party directory which contains code with testing.T/B calls from goroutines + go vet ./cmd/... + go vet ./internal/... + go vet ./pkg/... - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' + - name: Run go fmt check + working-directory: ./rigour + run: | + fmt_output=$(gofmt -l .) + if [ -n "$fmt_output" ]; then + echo "The following files are not formatted:" + echo "$fmt_output" + exit 1 + fi - - name: Install pre-commit - run: | - python -m pip install --upgrade pip - pip install pre-commit + - name: Run tests + working-directory: ./rigour + run: | + go test -v -race -coverprofile=coverage.out -covermode=atomic ./... - - name: Run pre-commit - run: pre-commit run --all-files + - name: Generate coverage report + working-directory: ./rigour + run: go tool cover -html=coverage.out -o coverage.html + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: ./rigour/coverage.out + flags: unittests + name: codecov-rigour + fail_ci_if_error: false + + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-report-go${{ matrix.go-version }} + path: rigour/coverage.html + + build: + name: Build Docker Images + runs-on: ubuntu-latest + needs: [test] + + steps: + - name: Checkout code + uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 82f9275..d4a450c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,162 +1,33 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +# MacOS filesysem files +.DS_Store -# C extensions +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll *.so +*.dylib -# 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 +# Test binary, built with `go test -c` +*.test -# 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 +# Output of the go coverage tool, specifically when used with LiteIDE +*.out -# Installer logs -pip-log.txt -pip-delete-this-directory.txt +# Dependency directories (remove the comment below to include it) +# vendor/ -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ +# Go workspace file +go.work -# Translations -*.mo -*.pot +# Dev folders +.vscode +.idea -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal +# rigour binary +rigour/rigour -# 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 - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments +# env .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/ -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +/data \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index c222ae6..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: mixed-line-ending - - id: check-added-large-files - - id: check-case-conflict - - - repo: https://github.com/ambv/black - rev: 24.10.0 - hooks: - - id: black - language_version: python3.12 - - - repo: https://github.com/pycqa/autoflake - rev: v2.3.1 - hooks: - - id: autoflake - - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - args: ["--profile", "black", "--filter-files"] - - - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 - hooks: - - id: pyupgrade - args: [--py39-plus] diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index d9c88d4..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "python.analysis.extraPaths": [ - "./rigour/common" - ] -} diff --git a/README.md b/README.md index 914ae27..5ca232f 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,103 @@ -# Rigour: An IoT Scanner Inspired by Shodan.io +# Rigour: An IoT Search Engine -Rigour is a comprehensive Internet of Things (IoT) scanning tool designed to discover, analyze, and report on devices connected to the internet. By leveraging powerful tools like ZMap and ZGrab, Rigour performs large-scale network scans to identify active hosts, retrieve service banners, and detect potential vulnerabilities. It offers both REST and streaming APIs for data access, and includes a user interface for data visualization. +[![GitHub License](https://img.shields.io/github/license/ctrlsam/rigour?style=flat-square)](LICENSE) +[![GitHub Issues](https://img.shields.io/github/issues/ctrlsam/rigour?style=flat-square)](https://github.com/ctrlsam/rigour/issues) +[![GitHub Stars](https://img.shields.io/github/stars/ctrlsam/rigour?style=flat-square)](https://github.com/ctrlsam/rigour) -## Get Started +Rigour is a comprehensive Internet of Things (IoT) scanning tool designed to discover, analyze, and report on devices connected to the internet. Rigour performs large-scale network scans to identify active hosts, retrieve service banners, and detect potential vulnerabilities. Rigour was inspired by [Shodan.io](https://www.shodan.io/), a popular IoT search engine. If you find this project useful, please consider starring the repository! -To quickly set up Rigour and its services, use Docker Compose: +> [!WARNING] +> Rigour is intended for ethical use only. Always obtain permission before scanning networks and devices that you do not own. Use this tool responsibly and in compliance with all applicable laws and regulations. -```bash -docker compose up -``` -This command initializes all components required for scanning, data processing, and data access. +## Get Started -## Architecture Overview +Before you begin, ensure you have the necessary prerequisites installed on your system. -![DiagramOverview](./docs/overview_diagram.png) +### Prerequisites -Rigour's architecture comprises several interconnected components that work in harmony to perform comprehensive network scanning and analysis. +* [Docker](https://www.docker.com/get-started) +* [Docker Compose](https://docs.docker.com/compose/install/) +* [MaxMind Account](./docs/MAXMIND_SETUP.md) -### Components +### Installation Steps -#### Port Scanner +1. **Clone the Repository**: -The **Port Scanner** uses [ZMap](https://github.com/zmap/zmap) to scan specified port ranges and identify active hosts on the internet. Discovered hosts are published to a RabbitMQ queue in the format `{country}.{port}.{ip}.port`. This allows other services to consume live data for further processing. Additionally, scan results are stored in the database for persistent access. + ```bash + git clone https://github.com/ctrlsam/rigour.git + cd rigour + ``` -#### Banner Grabber +### Run with Docker (Recommended) -The **Banner Grabber** employs [ZGrab](https://github.com/zmap/zgrab2) to retrieve service banners from the identified hosts, providing detailed information about running services (e.g., SSH, HTTP). This service subscribes to the `{country}.{port}.{ip}.port` queue and publishes the collected banners to `{country}.{port}.{ip}.banners`. The database entries for each host are updated with this new information. +1. **Configure Environment Variables**: + Create a `.env` file under the root directory and set the required environment variables as per the instructions in `.env.example`. + You will also want to set what IP range you want to scan. By default this is set as the ENTIRE internet so be careful! -#### Vulnerability Scanner + ```bash + cp .env.example .env + nano .env + ``` -The **Vulnerability Scanner** analyzes the collected banner data to detect vulnerable servers by cross-referencing with CVE databases. It examines identifiers such as HTTP server headers against known vulnerabilities. This service subscribes to the `{country}.{port}.{ip}.banners` queue and publishes its findings to `{country}.{port}.{ip}.vulns`. Host documents in the database are updated with vulnerability details. +2. **Run with Docker Compose**: + Ensure you have Docker and Docker Compose installed. Then, run: -### Data Access Interfaces + ```bash + docker compose up -d + ``` -#### REST API +3. **Access the UI**: + Open your web browser and navigate to `http://localhost:3000` to access the Rigour web interface. -Rigour provides a RESTful API to access the scanned host data programmatically. The API allows for querying hosts, services, and vulnerabilities. +4. **Stop the Services**: + To stop the services, run: + ```bash + docker compose down + ``` -- **Documentation**: Detailed API documentation is available [here](./api/README.md). +## Architecture Overview + +Rigour's architecture comprises several interconnected components that work in harmony to perform comprehensive network scanning and analysis. + +### Components -#### Streaming API +#### Crawler -For real-time data processing, Rigour offers a streaming API via RabbitMQ queues. +The Crawler is responsible for performing large-scale network scans using [Naabu](https://github.com/projectdiscovery/naabu) and fingerprinting the discovered devices with [Fingerprintx](https://github.com/praetorian-inc/fingerprintx). Results from this are published to Kafka for further processing. The microservice design was chosen to support multiple worker nodes in the future. -##### Queue Structure +#### Persistence -The RabbitMQ queues follow this naming convention: +The Persistence component consumes scan results and enriches them with other data sources such as ASN and location info from GeoIP. It then stores the enriched data in a MongoDB database. This allows for efficient querying and retrieval of scan data for analysis and reporting. -```bash -{country}.{port}.{ip}.{data_type} -``` +#### API -##### Message Components +The API component provides a RESTful interface for accessing scan data stored in MongoDB. It serves as the backend for the Rigour UI, enabling users to query and retrieve scan results. -- **`{country}`**: Two-letter country code (e.g., `US` for the United States) -- **`{port}`**: Port number being scanned (e.g., `443`) -- **`{ip}`**: IP address of the host (e.g., `192.168.1.1`) -- **`{data_type}`**: Type of data (`port`, `banners`, or `vulns`) +#### User Interface -##### Example +The Rigour UI provides an intuitive interface for viewing scan results. You can filter and search for specific devices, view detailed information about each device, and export scan results for further analysis. The app is build using Next.js and communicates with the API to fetch data. -```bash -US.443.192.168.1.1.port -``` +Rigour UI Screenshot -This structure facilitates easy identification and routing of data based on geographic location, port, host, and data type. +## REST API Documentation -### User Interface +### `GET /api/hosts/search` -A web-based **User Interface** is available for visualizing scan results and interacting with the data. +- **Description**: Search for hosts based on query parameters. +- **Query Parameters**: + - `filter` (optional, string): A JSON-encoded MongoDB-style query object used to filter hosts. The server applies this object directly as a MongoDB $match stage. + - `limit` (optional, integer): Maximum number of hosts to return. Defaults to 50. Minimum/invalid values default to 50. Maximum allowed value is 500. + - `page_token` (optional, string): Opaque pagination token returned from a previous response next_page_token. Pass this token to retrieve the next page of results. -> **Note**: The user interface is in early development stages and requires manual startup. +### `GET /api/facets` -## Considerations +- **Description**: Retrieve available facets for filtering search results. +- **Query Parameters**: + - `filter` (optional, string): A JSON-encoded MongoDB-style query object to restrict the aggregation to a subset of hosts. -1. **Network Capacity**: High scanning rates set in ZMap may consume significant network bandwidth, potentially causing other services to experience latency or connectivity issues. -2. **Process Resilience**: Current processes do not automatically resume after a crash. If a service like the Banner Grabber fails, it will not pick up where it left off upon restarting. Enhancements to address this limitation are planned for future releases. +## Acknowledgements -## Future Enhancements +We would like to thank the open-source community for their contributions and support in developing Rigour. -- **ISP/Organization Mapping**: Incorporate mapping of IP addresses to Internet Service Providers or organizations to provide more context. -- **DNS Mapping**: Implement DNS resolution to associate hostnames with IP addresses. -- **Campaign Configurability**: Introduce configurable scanning campaigns, allowing users to specify ports, protocols, and data types to capture. +Special thanks to the creators of [Fingerprintx](https://github.com/praetorian-inc/fingerprintx) and [Naabu](https://github.com/projectdiscovery/naabu) for their invaluable tools and resources. diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..48c2e5a --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,85 @@ +services: + # ============================================ + # Kafka + # ============================================ + + zookeeper: + image: confluentinc/cp-zookeeper:7.3.1 + container_name: zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "2181:2181" + networks: + - rigour-network + + kafka: + image: confluentinc/cp-kafka:7.3.1 + container_name: kafka + depends_on: + - zookeeper + ports: + - "9092:9092" + - "29092:29092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + networks: + - rigour-network + healthcheck: + test: ["CMD", "bash", "-lc", "kafka-topics --bootstrap-server kafka:9092 --list >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + + # kafka-ui: + # image: provectuslabs/kafka-ui:latest + # container_name: kafka-ui + # ports: + # - "9000:8080" + # environment: + # KAFKA_CLUSTERS_0_NAME: local + # KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 + # KAFKA_CLUSTERS_0_ZOOKEEPER: disabled + # networks: + # - rigour-network + + # ============================================ + # MongoDB + # ============================================ + + mongo: + image: mongo:latest + container_name: mongo + ports: + - "27017:27017" + volumes: + - mongo-data:/data/db + networks: + - rigour-network + + # ============================================ + # GeoIP Data Puller + # ============================================ + + geoipupdate: + container_name: geoipupdate + image: ghcr.io/maxmind/geoipupdate + restart: unless-stopped + environment: + - GEOIPUPDATE_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID} + - GEOIPUPDATE_LICENSE_KEY=${MAXMIND_LICENSE_KEY} + - 'GEOIPUPDATE_EDITION_IDS=GeoLite2-ASN GeoLite2-City' + - GEOIPUPDATE_FREQUENCY=72 + volumes: + - 'geoipupdate_data:/usr/share/GeoIP' + +volumes: + mongo-data: + geoipupdate_data: diff --git a/docker-compose.yml b/docker-compose.yml index 3af29e4..d5a38f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,170 +1,77 @@ services: - port-scanner: + crawler: build: - context: . - dockerfile: rigour/ports/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - PORTS: "80,443,22,21,25565,27017,143,6379" - NETWORKS: "10.0.0.0/8 192.168.0.0/16" + context: ./rigour + dockerfile: Dockerfile.crawler + container_name: rigour-crawler depends_on: - rabbitmq: + kafka: condition: service_healthy - - banner-scanner-http-80: - build: - context: . - dockerfile: rigour/banners/Dockerfile + command: + - "${CRAWLER_CIDR:-0.0.0.0/0}" + - "--kafka-brokers" + - "kafka:9092" + - "--top-ports" + - "${CRAWLER_DISCOVERY_TOP_PORTS:-1000}" + networks: + - rigour-network restart: unless-stopped - network_mode: "host" - environment: - SERVICE: http - PORT: 80 - depends_on: - rabbitmq: - condition: service_healthy - banner-scanner-http-443: + persistence: build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: http - PORT: 443 + context: ./rigour + dockerfile: Dockerfile.persistence + container_name: rigour-persistence depends_on: - rabbitmq: + kafka: condition: service_healthy - - banner-scanner-ssh-22: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: ssh - PORT: 22 - depends_on: - rabbitmq: + mongo: + condition: service_started + geoipupdate: condition: service_healthy - - banner-scanner-ftp-21: - build: - context: . - dockerfile: rigour/banners/Dockerfile + command: + - "--brokers" + - "kafka:9092" + - "--mongo-uri" + - "mongodb://mongo:27017" + - "--geoip-path" + - "/data/geoip" + volumes: + - geoipupdate_data:/data/geoip + networks: + - rigour-network restart: unless-stopped - network_mode: "host" - environment: - SERVICE: ftp - PORT: 21 - depends_on: - rabbitmq: - condition: service_healthy - banner-scanner-imap-143: + api: build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: imap - PORT: 143 + context: ./rigour + dockerfile: Dockerfile.api + container_name: rigour-api + ports: + - "8080:8080" + command: + - "--mongo-uri" + - "mongodb://mongo:27017" depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-redis-6379: - build: - context: . - dockerfile: rigour/banners/Dockerfile + - mongo + networks: + - rigour-network restart: unless-stopped - network_mode: "host" - environment: - SERVICE: redis - PORT: 6379 - depends_on: - rabbitmq: - condition: service_healthy - banner-scanner-mongodb-27017: + ui: build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: mongodb - PORT: 27017 + context: ./rigour-ui + container_name: rigour-ui depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-jarm-443: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" + - api + ports: + - "3000:3000" environment: - SERVICE: jarm - PORT: 443 - depends_on: - rabbitmq: - condition: service_healthy - - addon-minecraft-scanner: - build: - context: . - dockerfile: rigour/addons/minecraft/Dockerfile - restart: unless-stopped - network_mode: "host" - depends_on: - rabbitmq: - condition: service_healthy - - vuln-scanner: - build: - context: . - dockerfile: rigour/vuln/Dockerfile - restart: unless-stopped - network_mode: "host" - depends_on: - rabbitmq: - condition: service_healthy - - api: - build: - context: . - dockerfile: rigour/api/Dockerfile + - NEXT_PUBLIC_API_BASE_URL=http://api:8080 + networks: + - rigour-network restart: unless-stopped - network_mode: "host" - depends_on: - rabbitmq: - condition: service_healthy - mongodb: - image: mongo - restart: always - ports: - - "27017:27017" - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] - interval: 10s - timeout: 5s - retries: 5 - - rabbitmq: - image: rabbitmq:3-management - restart: unless-stopped - ports: - - "5672:5672" - - "15672:15672" - healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] - interval: 10s - timeout: 5s - retries: 5 +networks: + rigour-network: + driver: bridge diff --git a/docs/MAXMIND_SETUP.md b/docs/MAXMIND_SETUP.md new file mode 100644 index 0000000..1ca2f26 --- /dev/null +++ b/docs/MAXMIND_SETUP.md @@ -0,0 +1,33 @@ +# Get MaxMind License Key + +For GeoIP and ASN lookups, Rigour uses the MaxMind service. This service is completely free to use, but requires a license key. To obtain a MaxMind license key, follow these steps: + +1. **Create a MaxMind Account**: + - Setup a free account on the MaxMind website: [https://www.maxmind.com/en/geolite2/signup](https://www.maxmind.com/en/geolite2/signup). + +2. **Generate a License Key**: + - After logging into your MaxMind account, navigate to the "Manage License Keys" section. + - Click on "Create a New License Key". + - Provide a name for the key (e.g., "Rigour") + - Click Confirm to generate the key. + +3. **Record the License Key & Account ID**: + - Copy your Account ID and store it in your .env file as `MAXMIND_ACCOUNT_ID=your_account_id_here`. + - Copy the generated license key and store it in your .env file as `MAXMIND_LICENSE_KEY=your_license_key_here`. + +## Local Development + +If you are running Rigour locally without Docker, you will need to download the GeoIP databases manually, or use the `geoipupdate` tool provided by MaxMind. + +Follow these steps to set up the GeoIP databases: + +```shell +# Run at the root of the rigour project +docker run --rm \ + -e GEOIPUPDATE_ACCOUNT_ID=YOUR_ACCOUNT_ID \ + -e GEOIPUPDATE_LICENSE_KEY=YOUR_LICENSE_KEY \ + -e "GEOIPUPDATE_EDITION_IDS=GeoLite2-City GeoLite2-ASN" \ + -e GEOIPUPDATE_FREQUENCY=0 \ + -v ./data/geoip:/usr/share/GeoIP \ + ghcr.io/maxmind/geoipupdate +``` diff --git a/docs/overview_diagram.png b/docs/overview_diagram.png deleted file mode 100644 index 3fe9734..0000000 Binary files a/docs/overview_diagram.png and /dev/null differ diff --git a/docs/ui.png b/docs/ui.png new file mode 100644 index 0000000..6218d59 Binary files /dev/null and b/docs/ui.png differ diff --git a/rigour-ui/.dockerignore b/rigour-ui/.dockerignore new file mode 100644 index 0000000..fe6ed8b --- /dev/null +++ b/rigour-ui/.dockerignore @@ -0,0 +1,127 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage +.grunt + +# Bower dependency directory +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons +build/Release + +# Dependency directories +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.local +.env.production + +# parcel-bundler cache +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# IDE +.vscode +.idea +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Documentation +README.md +*.md + +# Test files +test/ +tests/ +__tests__/ +*.test.js +*.test.ts +*.spec.js +*.spec.ts +jest.config.js diff --git a/rigour-ui/.gitignore b/rigour-ui/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/rigour-ui/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/rigour-ui/Dockerfile b/rigour-ui/Dockerfile new file mode 100644 index 0000000..3b07092 --- /dev/null +++ b/rigour-ui/Dockerfile @@ -0,0 +1,51 @@ +FROM node:25-alpine AS deps + +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json package-lock.json /app/ +RUN npm ci + +# Rebuild the source code only when needed +FROM node:25-alpine AS builder + +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +RUN npm run build + +# Production image, copy all the files and run next +FROM node:25-alpine AS runner + +WORKDIR /app + +ENV NODE_ENV production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +############ +# Permissions to write files when executing entrypoint +RUN chown -R nextjs:nodejs /app + +# Handle Entrypoint +COPY --from=builder --chown=nextjs:nodejs /app/entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh +ENTRYPOINT ["/app/entrypoint.sh"] +############ + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 + +CMD ["node", "server.js"] diff --git a/rigour-ui/README.md b/rigour-ui/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/rigour-ui/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/rigour-ui/app/(dashboard)/host/[slug]/page.tsx b/rigour-ui/app/(dashboard)/host/[slug]/page.tsx new file mode 100644 index 0000000..57e495d --- /dev/null +++ b/rigour-ui/app/(dashboard)/host/[slug]/page.tsx @@ -0,0 +1,374 @@ +import Link from 'next/link'; +import { getHostByIP } from '../../../../lib/api'; +import { formatDate, formatDateShort } from '../../../../lib/utils'; +import { Host } from '../../../../lib/types'; +import { Card, CardContent, CardHeader } from '../../../../components/ui/card'; +import { Badge } from '../../../../components/ui/badge'; +import { Button } from '../../../../components/ui/button'; +import { + Globe, + Network, + Server, + Clock, + MapPin, + ChevronLeft, + ExternalLink, + AlertCircle, + CheckCircle, + Wifi, + Shield, +} from 'lucide-react'; + +interface Params { + slug: string; +} + +export default async function HostDetailsPage({ + params +}: { + params: Promise +}) { + const { slug } = await params; + let host: Host | null = null; + let error: string | null = null; + + try { + host = await getHostByIP(slug); + } catch (err) { + console.error('Failed to fetch host:', err); + error = err instanceof Error ? err.message : 'Failed to fetch host details'; + } + + if (error || !host) { + return ( +
+
+
+ + + +
+ +

+ {error || 'Host not found'} +

+
+
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+ + + +
+ ID: {host.id} +
+
+ + {/* Host Summary */} + + +
+
+
+

+ {host.ip} +

+

{host.asn.organization}

+
+
+ + {/* Key Information Grid */} +
+ {/* Country */} +
+
+ + Country +
+

{host.asn.country}

+
+ + {/* City */} +
+
+ + City +
+

{host.location.city}

+
+ + {/* ASN */} +
+
+ + ASN +
+

AS{host.asn.number}

+
+ + {/* Timezone */} +
+
+ + Timezone +
+

{host.location.timezone}

+
+
+
+
+
+ + {/* Timeline Information */} + + +

+ + Timeline +

+
+ +
+
+
+ First Seen +
+

{formatDate(host.first_seen)}

+

+ {formatDateShort(host.first_seen)} +

+
+
+
+ Last Seen +
+

{formatDate(host.last_seen)}

+

+ {formatDateShort(host.last_seen)} +

+
+
+
+
+ + {/* Geolocation Details */} + + +

+ + Geolocation +

+
+ +
+
+
+ Coordinates +
+

+ {host.location.coordinates[0].toFixed(4)}, {host.location.coordinates[1].toFixed(4)} +

+ + View on Google Maps + + +
+
+
+ ASN Information +
+
+

+ Number:{' '} + AS{host.asn.number} +

+

+ Org:{' '} + {host.asn.organization} +

+

+ Country:{' '} + {host.asn.country} +

+
+
+
+
+
+ + {/* Services */} + + +

+ + Services ({host.services.length}) +

+
+ + {host.services.length === 0 ? ( +

No services discovered

+ ) : ( +
+ {host.services.map((service, idx) => ( +
+ {/* Port and Protocol */} +
+
+ + {service.port} + + + {service.protocol} + + {service.tls && ( + + + TLS + + )} +
+
+ + {service.transport.toUpperCase()} +
+
+ + {/* Last Scan */} +
+ Last scanned: {formatDateShort(service.last_scan)} +
+ + {/* Service Details */} + {(service.https || service.http || service.ssh) && ( +
+ {service.https && ( +
+
+ + HTTPS +
+
+

+ Status Code: + + {service.https.statusCode} + +

+

+ Status: + {service.https.status} +

+ {Object.keys(service.https.responseHeaders).length > 0 && ( +
+

Headers:

+
+ {Object.entries(service.https.responseHeaders).map( + ([key, values]) => ( +

+ {key}:{' '} + {(values as string[]).join(', ')} +

+ ) + )} +
+
+ )} +
+
+ )} + + {service.http && ( +
+
+ + HTTP +
+
+

+ Status Code: + + {service.http.statusCode} + +

+

+ Status: + {service.http.status} +

+ {Object.keys(service.http.responseHeaders).length > 0 && ( +
+

Headers:

+
+ {Object.entries(service.http.responseHeaders).map( + ([key, values]) => ( +

+ {key}:{' '} + {(values as string[]).join(', ')} +

+ ) + )} +
+
+ )} +
+
+ )} + + {service.ssh && ( +
+
+ + SSH +
+
+

{service.ssh.banner}

+
+
+ )} +
+ )} +
+ ))} +
+ )} +
+
+ + {/* Raw Data Section */} + + +

Raw Data

+
+ +
+
+                {JSON.stringify(host, null, 2)}
+              
+
+
+
+
+
+ ); +} diff --git a/rigour-ui/app/(dashboard)/page.tsx b/rigour-ui/app/(dashboard)/page.tsx new file mode 100644 index 0000000..e42a308 --- /dev/null +++ b/rigour-ui/app/(dashboard)/page.tsx @@ -0,0 +1,152 @@ +import { SearchHeader } from '../../components/SearchHeader'; +import { FacetFilters } from '../../components/FacetFilters'; +import { HostResults } from '../../components/HostResults'; +import WorldMap from '../../components/ui/world-map'; +import { searchHosts, getFacets, FacetCounts, API_BASE_URL } from '../../lib/api'; +import { Host } from '../../lib/types'; + +interface PageProps { + searchParams: Promise>; +} + +export default async function Home({ searchParams: searchParamsPromise }: PageProps) { + const searchParams = await searchParamsPromise; + + let hosts: Host[] = []; + let facets: FacetCounts | undefined = undefined; + let error: string | null = null; + + try { + // Build filter from search params + const filter: Record = {}; + + // Parse filters from facet selections + const selectedCountries = searchParams.countries + ? Array.isArray(searchParams.countries) + ? searchParams.countries + : searchParams.countries.split(',') + : []; + const selectedASNs = searchParams.asns + ? Array.isArray(searchParams.asns) + ? searchParams.asns + : searchParams.asns.split(',') + : []; + const selectedServices = searchParams.services + ? Array.isArray(searchParams.services) + ? searchParams.services + : searchParams.services.split(',') + : []; + + if (selectedCountries.length > 0) { + filter['asn.country'] = { $in: selectedCountries }; + } + + if (selectedASNs.length > 0) { + const asnNumbers = selectedASNs.map(asn => parseInt(asn.replace('AS', ''))); + filter['asn.number'] = { $in: asnNumbers }; + } + + if (selectedServices.length > 0) { + filter['services.protocol'] = { $in: selectedServices }; + } + + // Parse query syntax filters (e.g., from "services.protocol: ssh") + if (searchParams.filter) { + const filterParams = Array.isArray(searchParams.filter) + ? searchParams.filter + : [searchParams.filter]; + + for (const filterParam of filterParams) { + try { + const parsedFilter = JSON.parse(filterParam); + // Merge with existing filter + Object.assign(filter, parsedFilter); + } catch (e) { + console.error('Failed to parse filter parameter:', e); + } + } + } + + // Perform search + const searchResult = await searchHosts(filter, 50); + hosts = searchResult.hosts || []; + + // Fetch facets for the current filter to show accurate counts + const facetsResult = await getFacets(filter); + facets = facetsResult.facets || {}; + } catch (err) { + console.error('Failed to fetch data:', err); + error = err instanceof Error ? err.message : 'Failed to fetch data'; + } + + // World Map dots data + const mapDots = hosts + .filter(host => host.location.city !== 'Unknown') + .map(host => ({ + start: { + lat: host.location.coordinates[1], + lng: host.location.coordinates[0], + label: host.ip, + }, + end: { + lat: host.location.coordinates[1], + lng: host.location.coordinates[0], + label: host.ip, + }, + })); + + if (error) { + return ( +
+
+
Error: {error}
+

+ Make sure the API is running at {API_BASE_URL} +

+
+
+ ); + } + + return ( +
+
+ + +
+ + +
+ {hosts.length > 0 && } + +
+
+
+
+ ); +} diff --git a/rigour-ui/app/favicon.ico b/rigour-ui/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/rigour-ui/app/favicon.ico differ diff --git a/rigour-ui/app/globals.css b/rigour-ui/app/globals.css new file mode 100644 index 0000000..5362054 --- /dev/null +++ b/rigour-ui/app/globals.css @@ -0,0 +1,185 @@ +@import "tailwindcss"; + +:root { + --font-size: 16px; + --background: #ffffff; + --foreground: oklch(0.145 0 0); + --card: #ffffff; + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: #030213; + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.95 0.0058 264.53); + --secondary-foreground: #030213; + --muted: #ececf0; + --muted-foreground: #717182; + --accent: #e9ebef; + --accent-foreground: #030213; + --destructive: #d4183d; + --destructive-foreground: #ffffff; + --border: rgba(0, 0, 0, 0.1); + --input: transparent; + --input-background: #f3f3f5; + --switch-background: #cbced4; + --font-weight-medium: 500; + --font-weight-normal: 400; + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: #030213; + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.85 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.4 0 0); + --input: oklch(0.25 0 0); + --ring: oklch(0.6 0 0); + --font-weight-medium: 500; + --font-weight-normal: 400; + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-input-background: var(--input-background); + --color-switch-background: var(--switch-background); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: 0px; + --radius-md: 0px; + --radius-lg: 0px; + --radius-xl: 0px; + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + + body { + @apply bg-background text-foreground; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} + +/** + * Base typography. This is not applied to elements which have an ancestor with a Tailwind text class. + */ +@layer base { + :where(:not(:has([class*=' text-']), :not(:has([class^='text-'])))) { + h1 { + font-size: var(--text-2xl); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + h2 { + font-size: var(--text-xl); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + h3 { + font-size: var(--text-lg); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + h4 { + font-size: var(--text-base); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + label { + font-size: var(--text-base); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + button { + font-size: var(--text-base); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + input { + font-size: var(--text-base); + font-weight: var(--font-weight-normal); + line-height: 1.5; + } + } +} + +html { + font-size: var(--font-size); +} diff --git a/rigour-ui/app/layout.tsx b/rigour-ui/app/layout.tsx new file mode 100644 index 0000000..c30bf72 --- /dev/null +++ b/rigour-ui/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Rigour", + description: "Internet-Connected Device Intelligence Platform", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/rigour-ui/components.json b/rigour-ui/components.json new file mode 100644 index 0000000..ce89abd --- /dev/null +++ b/rigour-ui/components.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@aceternity": "https://ui.aceternity.com/registry/{name}.json" + } +} diff --git a/rigour-ui/components/FacetFilters.tsx b/rigour-ui/components/FacetFilters.tsx new file mode 100644 index 0000000..5a40a08 --- /dev/null +++ b/rigour-ui/components/FacetFilters.tsx @@ -0,0 +1,256 @@ +'use client'; + +import { useState, useTransition } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; +import { Checkbox } from './ui/checkbox'; +import { Label } from './ui/label'; +import { ScrollArea } from './ui/scroll-area'; +import { Button } from './ui/button'; +import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { FacetCounts } from '../lib/api'; + +interface FacetFiltersProps { + facets: FacetCounts; + selectedCountries: string[]; + selectedASNs: string[]; + selectedServices: string[]; +} + +export function FacetFilters({ + facets, + selectedCountries: initialCountries, + selectedASNs: initialASNs, + selectedServices: initialServices, +}: FacetFiltersProps) { + const [expandedSections, setExpandedSections] = useState({ + countries: true, + asns: true, + services: true, + }); + // Local state for selections before applying + const [tempCountries, setTempCountries] = useState(initialCountries); + const [tempASNs, setTempASNs] = useState(initialASNs); + const [tempServices, setTempServices] = useState(initialServices); + + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + + const toggleSection = (section: keyof typeof expandedSections) => { + setExpandedSections(prev => ({ + ...prev, + [section]: !prev[section], + })); + }; + + const applyFilters = () => { + startTransition(() => { + const params = new URLSearchParams(); + + if (tempCountries.length > 0) { + params.append('countries', tempCountries.join(',')); + } + + if (tempASNs.length > 0) { + params.append('asns', tempASNs.join(',')); + } + + if (tempServices.length > 0) { + params.append('services', tempServices.join(',')); + } + + router.push(`?${params.toString()}`); + }); + }; + + const handleCountryToggle = (country: string) => { + setTempCountries(prev => + prev.includes(country) + ? prev.filter(c => c !== country) + : [...prev, country] + ); + }; + + const handleASNToggle = (asn: string) => { + setTempASNs(prev => + prev.includes(asn) + ? prev.filter(a => a !== asn) + : [...prev, asn] + ); + }; + + const handleServiceToggle = (service: string) => { + setTempServices(prev => + prev.includes(service) + ? prev.filter(s => s !== service) + : [...prev, service] + ); + }; + + // Check if there are unsaved changes + const hasChanges = + JSON.stringify(tempCountries) !== JSON.stringify(initialCountries) || + JSON.stringify(tempASNs) !== JSON.stringify(initialASNs) || + JSON.stringify(tempServices) !== JSON.stringify(initialServices); + + return ( +
+ {isPending && ( +
+
+ + Updating filters... +
+
+ )} + + + toggleSection('countries')}> + Country + {expandedSections.countries ? ( + + ) : ( + + )} + + + {expandedSections.countries && ( + + +
+ {facets.countries.map((country) => ( +
+
+ handleCountryToggle(country.code)} + className="flex-shrink-0" + /> + +
+ + {country.count} + +
+ ))} +
+
+
+ )} +
+ + + + toggleSection('asns')}> + ASN + {expandedSections.asns ? ( + + ) : ( + + )} + + + {expandedSections.asns && ( + + +
+ {facets.asns.map((asn) => { + const asnString = `AS${asn.code}`; + return ( +
+
+ handleASNToggle(asnString)} + className="flex-shrink-0" + /> + +
+ + {asn.count} + +
+ ); + })} +
+
+
+ )} +
+ + + + toggleSection('services')}> + Service + {expandedSections.services ? ( + + ) : ( + + )} + + + {expandedSections.services && ( + + +
+ {Object.entries(facets.services || {}).map(([service, count]) => ( +
+
+ handleServiceToggle(service)} + className="flex-shrink-0" + /> + +
+ + {count} + +
+ ))} +
+
+
+ )} +
+ + {hasChanges && ( + + )} +
+ ); +} diff --git a/rigour-ui/components/HostCard.tsx b/rigour-ui/components/HostCard.tsx new file mode 100644 index 0000000..8b23f38 --- /dev/null +++ b/rigour-ui/components/HostCard.tsx @@ -0,0 +1,104 @@ +import { Host } from '../lib/types'; +import { Card, CardContent, CardHeader } from './ui/card'; +import { Badge } from './ui/badge'; +import { Globe, Network, Server, Clock, MapPin } from 'lucide-react'; + +interface HostCardProps { + host: Host; + onClick?: () => void; +} + +export function HostCard({ host, onClick }: HostCardProps) { + const formatDate = (dateString: string) => { + return new Date(dateString).toISOString().split('T')[0]; + }; + + return ( + + +
+
+

{host.ip}

+
+
+ + {host.asn.country} +
+
+ + {host.location.city} +
+
+ + AS{host.asn.number} +
+
+ + {formatDate(host.last_seen)} +
+
+

{host.asn.organization}

+
+
+
+ +
+
+ + Services +
+
+ {host.services.map((service, idx) => ( +
+ + {service.port} + + {service.protocol} + {service.tls && ( + + TLS + + )} + {service.transport} +
+ ))} +
+
+ + {host.services.some(s => s.https || s.http || s.ssh) && ( +
+
Details
+
+ {host.services.map((service, idx) => { + if (service.https) { + return ( +
+ HTTPS: {service.https.statusCode} {service.https.status} +
+ ); + } + if (service.http) { + return ( +
+ HTTP: {service.http.statusCode} {service.http.status} +
+ ); + } + if (service.ssh) { + return ( +
+ SSH: {service.ssh.banner} +
+ ); + } + return null; + })} +
+
+ )} +
+
+ ); +} diff --git a/rigour-ui/components/HostResults.tsx b/rigour-ui/components/HostResults.tsx new file mode 100644 index 0000000..91c28b3 --- /dev/null +++ b/rigour-ui/components/HostResults.tsx @@ -0,0 +1,142 @@ +'use client'; + +import { useState } from 'react'; +import { Host } from '../lib/types'; +import { HostCard } from './HostCard'; +import { Button } from './ui/button'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +interface HostResultsProps { + hosts: Host[]; + totalCount: number; + isLoading?: boolean; +} + +const HOSTS_PER_PAGE = 10; + +export function HostResults({ hosts, totalCount, isLoading }: HostResultsProps) { + const [currentPage, setCurrentPage] = useState(1); + + const totalPages = Math.ceil(hosts.length / HOSTS_PER_PAGE); + const startIndex = (currentPage - 1) * HOSTS_PER_PAGE; + const endIndex = startIndex + HOSTS_PER_PAGE; + const paginatedHosts = hosts.slice(startIndex, endIndex); + + // Reset to page 1 when filters change + const handlePageChange = (page: number) => { + setCurrentPage(page); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + // Reset page when hosts change + if (currentPage > totalPages && totalPages > 0) { + setCurrentPage(1); + } + + const getPageNumbers = () => { + const pages: (number | string)[] = []; + const showPages = 5; + + if (totalPages <= showPages) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + if (currentPage <= 3) { + for (let i = 1; i <= 4; i++) { + pages.push(i); + } + pages.push('...'); + pages.push(totalPages); + } else if (currentPage >= totalPages - 2) { + pages.push(1); + pages.push('...'); + for (let i = totalPages - 3; i <= totalPages; i++) { + pages.push(i); + } + } else { + pages.push(1); + pages.push('...'); + pages.push(currentPage - 1); + pages.push(currentPage); + pages.push(currentPage + 1); + pages.push('...'); + pages.push(totalPages); + } + } + + return pages; + }; + + return ( +
+
+
+ Showing {startIndex + 1}-{Math.min(endIndex, hosts.length)} of{' '} + {hosts.length} results + {totalCount !== hosts.length && ( + + ({totalCount} total hosts) + + )} +
+
+ + {hosts.length === 0 ? ( +
+

No hosts found matching your criteria.

+
+ ) : ( + <> +
+ {paginatedHosts.map((host) => ( + + + + ))} +
+ + {totalPages > 1 && ( +
+ + + {getPageNumbers().map((page, index) => ( + typeof page === 'number' ? ( + + ) : ( + + {page} + + ) + ))} + + +
+ )} + + )} +
+ ); +} diff --git a/rigour-ui/components/SearchHeader.tsx b/rigour-ui/components/SearchHeader.tsx new file mode 100644 index 0000000..151a3c6 --- /dev/null +++ b/rigour-ui/components/SearchHeader.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { Search, Github } from 'lucide-react'; +import { Input } from './ui/input'; +import { Button } from './ui/button'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +interface SearchHeaderProps { + initialQuery?: string; +} + +export function SearchHeader({ initialQuery = '' }: SearchHeaderProps) { + const [query, setQuery] = useState(initialQuery); + const router = useRouter(); + + // Parse query syntax like "field: value" into filter parameters + const parseQuery = (queryString: string): Record | null => { + // Look for pattern like "field: value" or "field:value" + const match = queryString.match(/(\w+(?:\.\w+)*)\s*:\s*(.+)/); + + if (match) { + const [, field, value] = match; + return { [field]: value.trim() }; + } + + return null; + }; + + const handleSearch = () => { + const params = new URLSearchParams(); + + if (query) { + const parsedParams = parseQuery(query); + + if (parsedParams) { + // Use parsed filter as query parameter + for (const [key, value] of Object.entries(parsedParams)) { + params.append('filter', JSON.stringify({ [key]: value })); + } + } + } + + router.push(`?${params.toString()}`); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + return ( +
+
+

+ RIGOUR +

+ + + +
+ +
+

+ Internet-Connected Device Intelligence Platform +

+

+ Query using field syntax: field: value (e.g., services.protocol: ssh) +

+
+ +
+
+ + setQuery(e.target.value)} + onKeyPress={handleKeyPress} + className="pl-12 h-14 bg-secondary border-border text-white" + /> +
+ +
+
+ ); +} diff --git a/rigour-ui/components/ui/badge.tsx b/rigour-ui/components/ui/badge.tsx new file mode 100644 index 0000000..fd3a406 --- /dev/null +++ b/rigour-ui/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/rigour-ui/components/ui/button.tsx b/rigour-ui/components/ui/button.tsx new file mode 100644 index 0000000..1eb0821 --- /dev/null +++ b/rigour-ui/components/ui/button.tsx @@ -0,0 +1,62 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border border-input bg-background text-foreground shadow-sm hover:bg-accent hover:text-accent-foreground hover:border-accent dark:bg-background/50 dark:border-ring dark:text-foreground dark:hover:bg-input dark:hover:border-ring/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/rigour-ui/components/ui/card.tsx b/rigour-ui/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/rigour-ui/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/rigour-ui/components/ui/checkbox.tsx b/rigour-ui/components/ui/checkbox.tsx new file mode 100644 index 0000000..cb0b07b --- /dev/null +++ b/rigour-ui/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/rigour-ui/components/ui/input.tsx b/rigour-ui/components/ui/input.tsx new file mode 100644 index 0000000..8916905 --- /dev/null +++ b/rigour-ui/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/rigour-ui/components/ui/label.tsx b/rigour-ui/components/ui/label.tsx new file mode 100644 index 0000000..fb5fbc3 --- /dev/null +++ b/rigour-ui/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/rigour-ui/components/ui/scroll-area.tsx b/rigour-ui/components/ui/scroll-area.tsx new file mode 100644 index 0000000..8e4fa13 --- /dev/null +++ b/rigour-ui/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/rigour-ui/components/ui/world-map.tsx b/rigour-ui/components/ui/world-map.tsx new file mode 100644 index 0000000..9acd4cf --- /dev/null +++ b/rigour-ui/components/ui/world-map.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useRef } from "react"; +import { motion } from "motion/react"; +import DottedMap from "dotted-map"; + + +interface MapProps { + dots?: Array<{ + start: { lat: number; lng: number; label?: string }; + end: { lat: number; lng: number; label?: string }; + }>; + lineColor?: string; +} + +export default function WorldMap({ + dots = [], + lineColor = "#0ea5e9", +}: MapProps) { + const svgRef = useRef(null); + const map = new DottedMap({ height: 100, grid: "diagonal" }); + + //const { theme } = useTheme(); + const theme = "dark"; + + const svgMap = map.getSVG({ + radius: 0.22, + color: theme === "dark" ? "#FFFFFF40" : "#00000040", + shape: "circle", + backgroundColor: theme === "dark" ? "black" : "white", + }); + + const projectPoint = (lat: number, lng: number) => { + const x = (lng + 180) * (800 / 360); + const y = (90 - lat) * (400 / 180); + return { x, y }; + }; + + const createCurvedPath = ( + start: { x: number; y: number }, + end: { x: number; y: number } + ) => { + const midX = (start.x + end.x) / 2; + const midY = Math.min(start.y, end.y) - 50; + return `M ${start.x} ${start.y} Q ${midX} ${midY} ${end.x} ${end.y}`; + }; + + return ( +
+ world map + + {dots.map((dot, i) => { + const startPoint = projectPoint(dot.start.lat, dot.start.lng); + const endPoint = projectPoint(dot.end.lat, dot.end.lng); + return ( + + + + ); + })} + + + + + + + + + + + {dots.map((dot, i) => ( + + + + + + + + + + + + + + + + + ))} + +
+ ); +} diff --git a/rigour-ui/entrypoint.sh b/rigour-ui/entrypoint.sh new file mode 100644 index 0000000..d846c55 --- /dev/null +++ b/rigour-ui/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Replace placeholder in built files with the actual environment variable value +find /app/.next/ -type f -name '*.js' -exec sed -i "s|NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER|${NEXT_PUBLIC_API_BASE_URL}|g" {} + + +# Start the Next.js application +exec "$@" diff --git a/rigour-ui/eslint.config.mjs b/rigour-ui/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/rigour-ui/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/rigour-ui/lib/api.ts b/rigour-ui/lib/api.ts new file mode 100644 index 0000000..1b4d7eb --- /dev/null +++ b/rigour-ui/lib/api.ts @@ -0,0 +1,117 @@ +import { Host } from './types'; + +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080'; + +export interface CountryFacet { + code: string; + name: string; + count: number; +} + +export interface ASNFacet { + code: number; + name: string; + count: number; +} + +export interface SearchResponse { + hosts: Host[]; + facets?: FacetCounts; + next_page_token?: string; +} + +export interface FacetCounts { + services: Record; + countries: CountryFacet[]; + asns: ASNFacet[]; +} + +export interface FacetsResponse { + facets: FacetCounts; +} + +/** + * Search for hosts with optional filters and pagination + */ +export async function searchHosts( + filter?: Record, + limit: number = 20, + pageToken?: string +): Promise { + const params = new URLSearchParams(); + + if (filter && Object.keys(filter).length > 0) { + params.append('filter', JSON.stringify(filter)); + } + + if (limit) { + params.append('limit', limit.toString()); + } + + if (pageToken) { + params.append('page_token', pageToken); + } + + const url = `${API_BASE_URL}/api/hosts/search?${params.toString()}`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + return response.json(); +} + +/** + * Get facet aggregations with optional filters + */ +export async function getFacets( + filter?: Record +): Promise { + const params = new URLSearchParams(); + + if (filter && Object.keys(filter).length > 0) { + params.append('filter', JSON.stringify(filter)); + } + + const url = `${API_BASE_URL}/api/facets?${params.toString()}`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + return response.json(); +} + +/** + * Get a single host by IP address + */ +export async function getHostByIP(ip: string): Promise { + const url = `${API_BASE_URL}/api/hosts/${ip}`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + return response.json(); +} diff --git a/rigour-ui/lib/types.ts b/rigour-ui/lib/types.ts new file mode 100644 index 0000000..ba845e4 --- /dev/null +++ b/rigour-ui/lib/types.ts @@ -0,0 +1,43 @@ +export interface Service { + ip: string; + port: number; + protocol: string; + tls: boolean; + transport: string; + last_scan: string; + https?: { + status: string; + statusCode: number; + responseHeaders: Record; + }; + http?: { + status: string; + statusCode: number; + responseHeaders: Record; + }; + ssh?: { + banner: string; + }; +} + +export interface Host { + id: string; + ip: string; + ip_int: number; + asn: { + number: number; + organization: string; + country: string; + }; + location: { + coordinates: [number, number]; + city: string; + timezone: string; + country_code: string; + country_name: string; + }; + first_seen: string; + last_seen: string; + services: Service[]; +} + diff --git a/rigour-ui/lib/utils.ts b/rigour-ui/lib/utils.ts new file mode 100644 index 0000000..0f05921 --- /dev/null +++ b/rigour-ui/lib/utils.ts @@ -0,0 +1,14 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export function formatDate(dateString: string) { + return new Date(dateString).toLocaleString(); +} + +export function formatDateShort(dateString: string) { + return new Date(dateString).toISOString().split('T')[0]; +} diff --git a/rigour-ui/next.config.ts b/rigour-ui/next.config.ts new file mode 100644 index 0000000..58dd644 --- /dev/null +++ b/rigour-ui/next.config.ts @@ -0,0 +1,12 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone", + env: { + // Workaround to inject env vars into the build time files, to be replaced at container runtime + // See entrypoint.sh and Dockerfile for more details + NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL || 'NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER', + }, +}; + +export default nextConfig; diff --git a/rigour-ui/package-lock.json b/rigour-ui/package-lock.json new file mode 100644 index 0000000..e8d5d63 --- /dev/null +++ b/rigour-ui/package-lock.json @@ -0,0 +1,7083 @@ +{ + "name": "rigour-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rigour-ui", + "version": "0.1.0", + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotted-map": "^2.2.3", + "lucide-react": "^0.562.0", + "motion": "^12.23.26", + "next": "16.1.1", + "react": "19.2.3", + "react-dom": "19.2.3", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.1", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", + "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.1.tgz", + "integrity": "sha512-Ovb/6TuLKbE1UiPcg0p39Ke3puyTCIKN9hGbNItmpQsp+WX3qrjO3WaMVSi6JHr9X1NrmthqIguVHodMJbh/dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz", + "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz", + "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz", + "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz", + "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz", + "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz", + "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz", + "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", + "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", + "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", + "license": "MIT", + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", + "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", + "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/type-utils": "8.50.1", + "@typescript-eslint/utils": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.50.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", + "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz", + "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.50.1", + "@typescript-eslint/types": "^8.50.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", + "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz", + "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz", + "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", + "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", + "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.50.1", + "@typescript-eslint/tsconfig-utils": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz", + "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", + "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dotted-map": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dotted-map/-/dotted-map-2.2.3.tgz", + "integrity": "sha512-8hyOOHHLLVCcCisM3yb9hqp+3bJ7TSMcr1SfrUw8Wxp5UMqih35jIvUyagweCooJbz/EH1nC9GGuPysh7+YlAg==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^6.0.1", + "proj4": "^2.6.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.1.tgz", + "integrity": "sha512-55nTpVWm3qeuxoQKLOjQVciKZJUphKrNM0fCcQHAIOGl6VFXgaqeMfv0aKJhs7QtcnlAPhNVqsqRfRjeKBPIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.1.1", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.26.tgz", + "integrity": "sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.26", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", + "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.1", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.1", + "@next/swc-darwin-x64": "16.1.1", + "@next/swc-linux-arm64-gnu": "16.1.1", + "@next/swc-linux-arm64-musl": "16.1.1", + "@next/swc-linux-x64-gnu": "16.1.1", + "@next/swc-linux-x64-musl": "16.1.1", + "@next/swc-win32-arm64-msvc": "16.1.1", + "@next/swc-win32-x64-msvc": "16.1.1", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proj4": { + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.20.2.tgz", + "integrity": "sha512-ipfBRfQly0HhHTO7hnC1GfaX8bvroO7VV4KH889ehmADSE8C/qzp2j+Jj6783S9Tj6c2qX/hhYm7oH0kgXzBAA==", + "license": "MIT", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/ahocevar" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.2.0.tgz", + "integrity": "sha512-L6f5oQRAoLU1RwXz0Ab9mxsE7LtxeVB6AIR1lpkZMsOyg/JXeaxBaXa/FVCBZyNr9S9I4wkHrlZTklX+im+WMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.1.tgz", + "integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.50.1", + "@typescript-eslint/parser": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wkt-parser": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.5.2.tgz", + "integrity": "sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q==", + "license": "MIT" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/rigour-ui/package.json b/rigour-ui/package.json new file mode 100644 index 0000000..a24b16a --- /dev/null +++ b/rigour-ui/package.json @@ -0,0 +1,37 @@ +{ + "name": "rigour-ui", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotted-map": "^2.2.3", + "lucide-react": "^0.562.0", + "motion": "^12.23.26", + "next": "16.1.1", + "react": "19.2.3", + "react-dom": "19.2.3", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.1", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "typescript": "^5" + } +} diff --git a/rigour-ui/postcss.config.mjs b/rigour-ui/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/rigour-ui/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/rigour-ui/public/file.svg b/rigour-ui/public/file.svg new file mode 100644 index 0000000..16fe3d3 --- /dev/null +++ b/rigour-ui/public/file.svg @@ -0,0 +1 @@ + diff --git a/rigour-ui/public/globe.svg b/rigour-ui/public/globe.svg new file mode 100644 index 0000000..c7215fe --- /dev/null +++ b/rigour-ui/public/globe.svg @@ -0,0 +1 @@ + diff --git a/rigour-ui/public/next.svg b/rigour-ui/public/next.svg new file mode 100644 index 0000000..5bb00d4 --- /dev/null +++ b/rigour-ui/public/next.svg @@ -0,0 +1 @@ + diff --git a/rigour-ui/public/vercel.svg b/rigour-ui/public/vercel.svg new file mode 100644 index 0000000..5215157 --- /dev/null +++ b/rigour-ui/public/vercel.svg @@ -0,0 +1 @@ + diff --git a/rigour-ui/public/window.svg b/rigour-ui/public/window.svg new file mode 100644 index 0000000..d05e7a1 --- /dev/null +++ b/rigour-ui/public/window.svg @@ -0,0 +1 @@ + diff --git a/rigour-ui/tsconfig.json b/rigour-ui/tsconfig.json new file mode 100644 index 0000000..3a13f90 --- /dev/null +++ b/rigour-ui/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/rigour/Dockerfile.api b/rigour/Dockerfile.api new file mode 100644 index 0000000..1d95537 --- /dev/null +++ b/rigour/Dockerfile.api @@ -0,0 +1,34 @@ +# Multi-stage build for the API service +# Stage 1: Build +FROM golang:1.24.0-alpine AS builder + +WORKDIR /build + +# Install build dependencies +RUN apk add --no-cache git make + +# Copy source code +COPY . . + +# Download dependencies +RUN go mod download + +# Build the api binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o api ./cmd/api + +# Stage 2: Runtime +FROM alpine:latest + +WORKDIR /app + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates + +# Copy binary from builder +COPY --from=builder /build/api . + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD ["/app/api", "--help"] + +ENTRYPOINT ["/app/api"] diff --git a/rigour/Dockerfile.crawler b/rigour/Dockerfile.crawler new file mode 100644 index 0000000..1883f68 --- /dev/null +++ b/rigour/Dockerfile.crawler @@ -0,0 +1,34 @@ +# Multi-stage build for the crawler service +# Stage 1: Build +FROM golang:1.24.0-alpine AS builder + +WORKDIR /build + +# Install build dependencies +RUN apk add --no-cache git make gcc musl-dev libpcap-dev + +# Copy source code +COPY . . + +# Download dependencies +RUN go mod download + +# Build the crawler binary +RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -o crawler ./cmd/crawler + +# Stage 2: Runtime +FROM alpine:latest + +WORKDIR /app + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates libpcap + +# Copy binary from builder +COPY --from=builder /build/crawler . + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD ["/app/crawler", "--help"] + +ENTRYPOINT ["/app/crawler"] diff --git a/rigour/Dockerfile.persistence b/rigour/Dockerfile.persistence new file mode 100644 index 0000000..6c331a0 --- /dev/null +++ b/rigour/Dockerfile.persistence @@ -0,0 +1,34 @@ +# Multi-stage build for the crawler service +# Stage 1: Build +FROM golang:1.24.0-alpine AS builder + +WORKDIR /build + +# Install build dependencies +RUN apk add --no-cache git make + +# Copy source code +COPY . . + +# Download dependencies +RUN go mod download + +# Build the persistence binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o persistence ./cmd/persistence + +# Stage 2: Runtime +FROM alpine:latest + +WORKDIR /app + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates + +# Copy binary from builder +COPY --from=builder /build/persistence . + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD ["/app/persistence", "--help"] + +ENTRYPOINT ["/app/persistence"] diff --git a/rigour/addons/minecraft/Dockerfile b/rigour/addons/minecraft/Dockerfile deleted file mode 100644 index a8a2285..0000000 --- a/rigour/addons/minecraft/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3.12-alpine3.20 - -WORKDIR /app - -# Install the common module -COPY rigour/common/ /app/common/ -RUN pip install --no-cache-dir -e ./common - -# Install ports dependencies -COPY rigour/addons/minecraft /app/ -RUN pip install --no-cache-dir --upgrade -r ./requirements.txt - -ENTRYPOINT ["python3", "main.py"] diff --git a/rigour/addons/minecraft/main.py b/rigour/addons/minecraft/main.py deleted file mode 100644 index a9cd172..0000000 --- a/rigour/addons/minecraft/main.py +++ /dev/null @@ -1,57 +0,0 @@ -import asyncio -from dataclasses import asdict - -from common import utils -from common.database.mongodb import Database -from common.queue.rabbitmq_asyncio import AsyncRabbitMQQueueManager -from common.types import Banner, HostMessage -from dacite import from_dict -from loguru import logger -from mcstatus import JavaServer, status_response - - -class MinecraftBannerGrabber: - def __init__(self, port: int = 25565): - self.db = Database() - self.queue = AsyncRabbitMQQueueManager() - self.port = port - - async def listen(self): - routing_key = f"#.{self.port}.#.port" - await self.queue.consume(routing_key=routing_key, callback=self.process_port) - - async def process_port(self, port_message: dict) -> None: - logger.debug(f"Received RabbitMQ message: {port_message}") - message = from_dict(data_class=HostMessage, data=port_message) - - banner = self.get_mc_banner(message.ip, message.port) - if banner is None: - logger.debug("Skipping as no banner found") - return - - logger.info(f"Found banner: {banner.raw}") - message.host.banner = Banner( - service="minecraft", port=message.port, data=dict(banner.raw) - ) - - route_key = utils.route_key_from_host_message(message, "banner") - await self.queue.publish(route_key, asdict(message)) - utils.save_banner(self.db, message) - - def get_mc_banner( - self, ip: str, port: int - ) -> status_response.JavaStatusResponse | None: - try: - server = JavaServer(ip, port) - status = server.status() - except: - logger.debug(f"Failed to get status of: {ip}:{port}") - else: - return status - - -if __name__ == "__main__": - grabber = MinecraftBannerGrabber() - loop = asyncio.get_event_loop() - loop.run_until_complete(grabber.listen()) - loop.run_forever() diff --git a/rigour/addons/minecraft/requirements.txt b/rigour/addons/minecraft/requirements.txt deleted file mode 100644 index d063b90..0000000 --- a/rigour/addons/minecraft/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mcstatus diff --git a/rigour/api/Dockerfile b/rigour/api/Dockerfile deleted file mode 100644 index 206d4ec..0000000 --- a/rigour/api/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3.12-alpine3.20 - -WORKDIR /app - -# Install the common module -COPY rigour/common/ /app/common/ -RUN pip install --no-cache-dir -e ./common - -# Install ports dependencies -COPY rigour/api /app/ -RUN pip install --no-cache-dir --upgrade -r ./requirements.txt - -CMD ["python", "-m", "uvicorn", "main:app", \ - "--host", "0.0.0.0", \ - "--port", "1234"] diff --git a/rigour/api/README.md b/rigour/api/README.md deleted file mode 100644 index df43ae5..0000000 --- a/rigour/api/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# REST API Documentation - -This documentation provides an overview of the REST API endpoints available for searching and retrieving host information. -The API allows users to perform searches with advanced query syntax, retrieve counts, and get detailed host data by IP address. - -## Base URL - -All endpoints are accessible under the following base URL: - -``` -http://:1234/api/v1 -``` - ---- - -## Endpoints - -### 1. Search Hosts - -**Endpoint:** - -``` -GET /host/search -``` - -**Description:** - -Search for hosts using a query syntax with optional filters and retrieve summary information through facets. - -**Query Parameters:** - -- `query` (string, optional): Search query with optional filters in the `filter:value` format. For example, to find servers in Germany: `location.country_code:DE`. - -- `skip` (integer, optional): Number of records to skip. Default is `0`. Must be greater than or equal to `0`. - -- `limit` (integer, optional): Maximum number of records to return. Default is `10`. Must be between `1` and `100`. - -**Response:** - -- **Status Code:** `200 OK` -- **Body:** A list of host objects matching the search criteria. - -**Response Model:** - -A list of `Host` objects. The structure of `Host` includes host details such as IP address, port information, and metadata. - -**Example Request:** - -``` -GET /api/v1/host/search?query=apache%20country:DE&skip=0&limit=10 -``` - ---- - -### 2. Get Hosts Count - -**Endpoint:** - -``` -GET /host/count -``` - -**Description:** - -Retrieve the total number of hosts that match the search query, along with any requested facet information. This endpoint does not return host details. - -**Query Parameters:** - -- `query` (string, optional): Search query with optional filters in the `filter:value` format. For example: `location.country_code:DE`. - -- `facet` (string, optional): Comma-separated list of properties for faceted search, optionally with counts. Example: `country:100`. - -**Response:** - -- **Status Code:** `200 OK` -- **Body:** A JSON object containing the total count and facet information. - -**Response Format:** - -```json -{ - "total": , - "facets": { - "": [ - { - "_id": "", - "count": - }, - ... - ], - ... - } -} -``` - -**Example Request:** - -``` -GET /api/v1/host/count?query=apache%20country:DE&facet=country:10 -``` - -**Example Response:** - -```json -{ - "total": 500, - "facets": { - "country": [ - { "_id": "DE", "count": 300 }, - { "_id": "US", "count": 100 }, - { "_id": "FR", "count": 50 } - ] - } -} -``` - ---- - -### 3. Get Host by IP - -**Endpoint:** - -``` -GET /host/{ip} -``` - -**Description:** - -Retrieve detailed host information by specifying the IP address. - -**Path Parameters:** - -- `ip` (string, required): The IP address of the host. - -**Response:** - -- **Status Code:** `200 OK` - - **Body:** A `Host` object containing host details. -- **Status Code:** `404 Not Found` - - **Body:** `{ "detail": "Host not found" }` -- **Status Code:** `500 Internal Server Error` - - **Body:** `{ "detail": "Invalid host data" }` - -**Example Request:** - -``` -GET /api/v1/host/192.168.1.1 -``` - ---- - -## Models - -### Host - -The `Host` model represents the structure of a host object returned by the API. It includes fields such as: - -- `ip` (string): The IP address of the host. -- `port` (integer): The port number. -- `data` (string): Banner data or service information. -- `timestamp` (string): The timestamp when the data was collected. -- `location` (object): Geolocation data including country, city, latitude, and longitude. -- Additional metadata fields as defined in the model. - -_Note: The exact structure may vary based on the data stored in the database._ diff --git a/rigour/api/main.py b/rigour/api/main.py deleted file mode 100644 index e6b96d2..0000000 --- a/rigour/api/main.py +++ /dev/null @@ -1,166 +0,0 @@ -from common.database.mongodb import Database -from common.types import DBHost -from fastapi import APIRouter, Depends, FastAPI, HTTPException, Query -from pydantic import ValidationError -from utils import build_facet_stages, parse_query_filters, process_host_document - -db = Database() - -router = APIRouter() - - -class PaginationParams: - def __init__( - self, skip: int = Query(0, ge=0), limit: int = Query(10, ge=1, le=100) - ): - self.skip = skip - self.limit = limit - - -@router.get("/host/search", response_model=list[DBHost]) -async def get_hosts( - query: str | None = Query( - default=None, - description="Search query with optional filters in 'filter:value' format", - ), - facet: str | None = Query( - default=None, - description="Comma-separated list of properties for faceted search, optionally\ - with counts (e.g., 'country:100')", - ), - pagination: PaginationParams = Depends(), -): - """ - Search using query syntax and use facets to get summary information for different - properties. - - Args: - query: Shodan search query. The provided string is used to search the database - of banners in Shodan, with the additional option to provide filters inside - the search query using a "filter:value" format. For example, the following - search query would find Apache Web servers located in Germany: - "apache country:DE". - - facet: A comma-separated list of properties to get summary information on. - Property names can also be in the format of "property:count", where "count" - is the number of facets that will be returned for a property (i.e. - "country:100" to get the top 100 countries for a search query). - """ - pipeline = [] - - # Build match stage from query if provided - if query: - match_conditions = parse_query_filters(query) - pipeline.append({"$match": match_conditions}) - - # if facet: - # TODO: Add facet stages to pipeline - - else: - # Without facets, we can use a simpler pipeline - pipeline.extend([{"$skip": pagination.skip}, {"$limit": pagination.limit}]) - - # Execute aggregation - return list(db.scans.aggregate(pipeline)) - - -@router.get("/host/count") -async def get_hosts_count( - query: str | None = Query( - default=None, - description="Search query with optional filters in 'filter:value' format", - ), - facet: str | None = Query( - default=None, - description="Comma-separated list of properties for faceted search, optionally\ - with counts (e.g., 'country:100')", - ), -) -> dict: - """ - This method behaves identical to "/host/search" with the only difference - that this method does not return any host results, it only returns the total - number of results that matched the query and any facet information that was - requested. - - Args: - query: The provided string is used to search the database of banners, with - the additional option to provide filters inside the search query using - a "filter:value" format. For example, the following search query would - find Apache Web servers located in Germany: "location.country_name:DE". - - facet: A comma-separated list of properties to get summary information on. - Property names can also be in the format of "property:count", where "count" - is the number of facets that will be returned for a property (i.e. - "country:100" to get the top 100 countries for a search query). - - db: AsyncIOMotorDatabase instance for MongoDB access - - Returns: - dict: Contains total count and facet information if requested - """ - pipeline = [] - - # Build match stage from query if provided - if query: - match_conditions = parse_query_filters(query) - pipeline.append({"$match": match_conditions}) - - # Initialize response structure - response = {"total": 0, "facets": {}} - - # Add facet stages if facets are requested - if facet: - pipeline.append({"$facet": build_facet_stages(facet)}) - else: - # If no facets requested, just get the count - pipeline.append({"$count": "total"}) - - # Execute aggregation - result = list(db.scans.aggregate(pipeline)) - - if result: - if facet: - # Extract results from faceted query - response["total"] = ( - result[0]["total"][0]["count"] if result[0]["total"] else 0 - ) - # Remove the total from facets before returning - result[0].pop("total", None) - response["facets"] = result[0] - else: - # Extract results from simple count query - response["total"] = result[0]["total"] - - return response - - -@router.get("/host/{ip}", response_model=DBHost) -async def get_host_by_ip(ip: str): - """ - Get host information by IP address - - Args: - ip: IP address of the host to retrieve - """ - host = db.scans.find_one({"ip": ip}) - - if not host: - raise HTTPException(status_code=404, detail="Host not found") - - host = process_host_document(host) - - try: - host_model = DBHost(**host) - except ValidationError: - raise HTTPException(status_code=500, detail="Invalid host data") - - return host_model - - -app = FastAPI(root_path="/api") -app.include_router(router, prefix="/v1") - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=1234) diff --git a/rigour/api/requirements.txt b/rigour/api/requirements.txt deleted file mode 100644 index f54f004..0000000 --- a/rigour/api/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pymongo -uvicorn -fastapi diff --git a/rigour/api/utils.py b/rigour/api/utils.py deleted file mode 100644 index ebdd094..0000000 --- a/rigour/api/utils.py +++ /dev/null @@ -1,88 +0,0 @@ -def process_host_document(doc) -> dict: - """ - Process a MongoDB document to remove the _id field and return a dictionary. - """ - doc = dict(doc) - doc.pop("_id", None) # Remove MongoDB's _id field - return doc - - -def parse_query_filters(query: str) -> dict: - """ - Parse the query string to extract filters and build MongoDB match conditions. - - Args: - query: Query string with optional filters in 'filter:value' format - - Returns: - dict: MongoDB match conditions - """ - conditions = {} - - # Split query into parts by whitespace, preserving quoted strings - import shlex - - query_parts = shlex.split(query) - - for part in query_parts: - if ":" in part: - field, value = part.split(":", 1) - # Handle special cases and type conversion as needed - if value.lower() in ("true", "false"): - value = value.lower() == "true" - elif value.isdigit(): - value = int(value) - conditions[field] = value - else: - # Add text search condition for non-filter parts - if "text" not in conditions: - conditions["text"] = {"$search": part} - else: - # Append to existing text search - conditions["text"]["$search"] += f" {part}" - - return conditions - - -def build_facet_stages(facet: str | None = None) -> dict: - """ - Build MongoDB facet stages from facet parameter. - - Args: - facet: Comma-separated list of properties for faceted search - - Returns: - dict: Facet stages for MongoDB aggregation pipeline - """ - if not facet: - return {} - - facet_stages = {"total": [{"$count": "count"}]} - - for facet_item in facet.split(","): - field_parts = facet_item.strip().split(":") - field_name = field_parts[0] - limit = int(field_parts[1]) if len(field_parts) > 1 else 10 - - # Handle nested fields using $getField - group_field = field_name - if "." in field_name: - field_parts = field_name.split(".") - group_field = { - "$getField": { - "field": field_parts[-1], - "input": { - "$getField": {"field": field_parts[0], "input": "$$ROOT"} - }, - } - } - else: - group_field = f"${field_name}" - - facet_stages[field_name.replace(".", "_")] = [ # type: ignore - {"$group": {"_id": group_field, "count": {"$sum": 1}}}, - {"$sort": {"count": -1}}, - {"$limit": limit}, - ] - - return facet_stages diff --git a/rigour/banners/Dockerfile b/rigour/banners/Dockerfile deleted file mode 100644 index d1f5ffb..0000000 --- a/rigour/banners/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# ============================================== -# Stage 1: Build ZGrab from source -# ============================================== -ARG GO_VERSION=1.20 -ARG PYTHON_VERSION=3.12 -FROM golang:${GO_VERSION}-alpine3.16 as build - -# System dependencies -RUN apk add --no-cache make git - -WORKDIR /usr/src/zgrab2 - -# Clone the ZMap repository -RUN git clone https://github.com/zmap/zgrab2.git . - -# Copy and cache deps -RUN go mod download && go mod verify - -# Build the actual app -RUN make all - -# =========================================================== -# Stage 2: Create the final image with runtime dependencies -# =========================================================== -FROM python:${PYTHON_VERSION}-alpine3.20 as run - -COPY --from=build /usr/src/zgrab2/cmd/zgrab2/zgrab2 /usr/bin/zgrab2 -ENV PATH="/usr/bin/zgrab2:${PATH}" - -WORKDIR /app - -# Install the common module -COPY rigour/common/ /app/lib/common/ -RUN pip install --no-cache-dir -e /app/lib/common - -# Install ports dependencies -COPY rigour/banners /app/ -RUN pip install --no-cache-dir --upgrade -r ./requirements.txt - -ENTRYPOINT ["python3", "main.py"] diff --git a/rigour/banners/main.py b/rigour/banners/main.py deleted file mode 100644 index 72aa2de..0000000 --- a/rigour/banners/main.py +++ /dev/null @@ -1,177 +0,0 @@ -import asyncio -import os -from dataclasses import asdict -from datetime import datetime, timedelta - -from common import utils -from common.database.mongodb import Database -from common.queue.rabbitmq_asyncio import AsyncRabbitMQQueueManager -from common.types import Banner, HostMessage -from dacite import from_dict -from loguru import logger -from zgrab import ZGrab, ZGrabCommand, ZGrabResult - - -class PendingMessage: - def __init__(self, message: HostMessage, timestamp: datetime): - self.message = message - self.timestamp = timestamp - - -class BannerGrabber: - def __init__(self, command: ZGrabCommand, message_timeout: int = 300): - self.command = command - self.zgrab = ZGrab(command) - self.db = Database() - self.queue = AsyncRabbitMQQueueManager() - self.run_task = None - self.cleanup_task = None - self.pending_messages: dict[str, PendingMessage] = {} - self.message_timeout = message_timeout # seconds - self.running = True - self.tasks = set() - - def _create_task(self, coro): - task = asyncio.create_task(coro) - self.tasks.add(task) - task.add_done_callback(self.tasks.discard) - return task - - async def shutdown(self, signal=None): - """Cleanup tasks tied to the service's shutdown.""" - if signal: - logger.info(f"Received exit signal {signal.name}") - - logger.info("Shutting down gracefully...") - - self.running = False - - # Cancel our cleanup task if it's still running - if self.cleanup_task and not self.cleanup_task.done(): - self.cleanup_task.cancel() - - # Cancel ZGrab run task if it's still running - if self.run_task and not self.run_task.done(): - self.run_task.cancel() - - # Cancel all remaining tasks - tasks = [t for t in self.tasks if not t.done()] - if tasks: - logger.info(f"Cancelling {len(tasks)} outstanding tasks") - for task in tasks: - task.cancel() - await asyncio.gather(*tasks, return_exceptions=True) - - logger.info("Shutdown complete.") - - async def listen(self, port: int | None = None): - self.run_task = self._create_task( - self.zgrab.run(callback=self.process_zmap_result) - ) - self.cleanup_task = self._create_task(self.cleanup_stale_messages()) - - logger.info(f"Starting consumer for port: {port}") - routing_key = f"#.{port if port else '#'}.#.port" - - await self.queue.consume( - routing_key=routing_key, callback=self.process_incoming - ) # Blocking - - async def process_incoming(self, port_message: dict): - logger.debug( - f"Received RabbitMQ message: {port_message}, now {str(len(self.pending_messages) + 1)} messages in queue" - ) - try: - message = from_dict(data_class=HostMessage, data=port_message) - self.pending_messages[message.ip] = PendingMessage( - message=message, timestamp=datetime.now() - ) - await self.zgrab.pipe(message.ip) - except Exception as e: - logger.error(f"Error processing incoming message: {e}") - - async def process_zmap_result(self, result: ZGrabResult): - logger.info(f"Received ZMap result: {result}") - try: - pending = self.pending_messages.pop(result.ip, None) - if pending is None: - logger.warning(f"No pending message found for IP: {result.ip}") - return - - message = pending.message - message.host.banner = Banner( - service=self.command.service, - port=self.command.port, - data=asdict(result.data)[self.command.service], - ) - - await self.publish(message) - utils.save_banner(self.db, message) - - except Exception as e: - logger.error(f"Error processing ZMap result: {e}") - - async def cleanup_stale_messages(self): - while self.running: - try: - current_time = datetime.now() - stale_ips = [ - ip - for ip, pending in self.pending_messages.items() - if (current_time - pending.timestamp) - > timedelta(seconds=self.message_timeout) - ] - - for ip in stale_ips: - self.pending_messages.pop(ip) - logger.warning( - f"Removed stale message for IP {ip} after {self.message_timeout} seconds" - ) - - await asyncio.sleep(60) # Check every minute - - except asyncio.CancelledError: - break - except Exception as e: - logger.error(f"Error in cleanup task: {e}") - await asyncio.sleep(60) # Continue cleanup even if there's an error - - async def publish(self, message: HostMessage): - routing_key = ( - f"{message.host.location.country_code}.{message.port}.{message.ip}.banner" - ) - try: - await self.queue.publish(routing_key, asdict(message)) - except Exception as e: - logger.error(f"Error publishing message: {e}") - - -def main(): - service = os.environ.get("SERVICE") - if not service: - logger.error("SERVICE environment variable is required") - return - - port = os.environ.get("PORT") - port = int(port) if port else None - - message_timeout = int(os.environ.get("MESSAGE_TIMEOUT", "300")) - - logger.info(f"Starting banner grabber for service: {service}, port: {port}") - - command = ZGrabCommand(service=service, port=port) - grabber = BannerGrabber(command, message_timeout=message_timeout) - - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(grabber.listen(port)) - loop.run_forever() - except KeyboardInterrupt: - logger.info("Received keyboard interrupt.") - finally: - logger.info("Exiting...") - loop.run_until_complete(grabber.shutdown()) - - -if __name__ == "__main__": - main() diff --git a/rigour/banners/requirements.txt b/rigour/banners/requirements.txt deleted file mode 100644 index 98c7fdd..0000000 --- a/rigour/banners/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pika -pymongo diff --git a/rigour/banners/zgrab.py b/rigour/banners/zgrab.py deleted file mode 100644 index 526d02a..0000000 --- a/rigour/banners/zgrab.py +++ /dev/null @@ -1,79 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -from common.subprocess import AsyncSubprocessBase -from dacite import from_dict - - -@dataclass -class ZGrabCommand: - service: str - port: int | None = None - - def build(self) -> list[str]: - args = ["zgrab2", self.service] - # args = ["docker", "run", "--rm", "-i", "ghcr.io/zmap/zgrab2", self.service] - if self.port: - args += ["--port", str(self.port)] - - args.append("--flush") - - return args - - -@dataclass -class ZGrabService: - status: str - protocol: str - timestamp: datetime - error: str | None = None - result: dict | None = None - - -@dataclass -class ZGrabData: - amqp091: ZGrabService | None = None - bacnet: ZGrabService | None = None - banner: ZGrabService | None = None - dnp3: ZGrabService | None = None - fox: ZGrabService | None = None - ftp: ZGrabService | None = None - http: ZGrabService | None = None - imap: ZGrabService | None = None - ipp: ZGrabService | None = None - jarm: ZGrabService | None = None - modbus: ZGrabService | None = None - mongodb: ZGrabService | None = None - mssql: ZGrabService | None = None - multiple: ZGrabService | None = None - mysql: ZGrabService | None = None - ntp: ZGrabService | None = None - oracle: ZGrabService | None = None - pop3: ZGrabService | None = None - postgres: ZGrabService | None = None - redis: ZGrabService | None = None - siemens: ZGrabService | None = None - smb: ZGrabService | None = None - smtp: ZGrabService | None = None - ssh: ZGrabService | None = None - telnet: ZGrabService | None = None - tls: ZGrabService | None = None - - -@dataclass -class ZGrabResult: - ip: str - data: ZGrabData - - -class ZGrab(AsyncSubprocessBase[ZGrabResult]): - def __init__(self, command: ZGrabCommand): - super().__init__(command, enable_piping=True) - - async def _parse_result(self, result: dict) -> ZGrabResult: - """Parse the ZGrab result into ZGrabResult dataclass.""" - result["data"][self.command.service]["timestamp"] = datetime.fromisoformat( - result["data"][self.command.service]["timestamp"] - ) - - return from_dict(data_class=ZGrabResult, data=result) diff --git a/rigour/cmd/api/main.go b/rigour/cmd/api/main.go new file mode 100644 index 0000000..7aacf22 --- /dev/null +++ b/rigour/cmd/api/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "time" + + "github.com/ctrlsam/rigour/internal/api" + "github.com/ctrlsam/rigour/internal/storage" + "github.com/ctrlsam/rigour/internal/storage/mongodb" + "github.com/spf13/cobra" +) + +type cliConfig struct { + mongoURI string + database string + collection string + addr string +} + +var config cliConfig + +var rootCmd = &cobra.Command{ + Use: "rigour-api", + Short: "REST API server for Rigour", + Long: "A REST API server for querying scanned hosts and services from MongoDB", + RunE: func(cmd *cobra.Command, args []string) error { + return runServer(cmd.Context()) + }, +} + +func init() { + rootCmd.Flags().StringVar(&config.mongoURI, "mongo-uri", "mongodb://localhost:27017", "MongoDB connection URI") + rootCmd.Flags().StringVar(&config.database, "mongo-db", "rigour", "MongoDB database name") + rootCmd.Flags().StringVar(&config.collection, "mongo-collection", "hosts", "MongoDB collection name") + rootCmd.Flags().StringVar(&config.addr, "addr", ":8080", "Server address (host:port)") +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func runServer(ctx context.Context) error { + // Validate inputs + if config.mongoURI == "" { + return fmt.Errorf("mongo-uri is required") + } + + // Create MongoDB client + connectCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + mongoClient, err := mongodb.NewClient(connectCtx, config.mongoURI, 10*time.Second) + if err != nil { + return fmt.Errorf("failed to connect to MongoDB: %w", err) + } + defer mongoClient.Close(context.Background()) + + // Create repository + repository, err := mongoClient.NewHostsRepository(connectCtx, storage.RepositoryConfig{ + Database: config.database, + Collection: config.collection, + Timeout: 10, + }) + if err != nil { + return fmt.Errorf("failed to create repository: %w", err) + } + + // Create router and handler + router := api.NewRouter(repository) + + // Setup HTTP server + server := &http.Server{ + Addr: config.addr, + Handler: router.Handler(), + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + // Start server in a goroutine + go func() { + fmt.Printf("Starting API server on %s\n", config.addr) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Fprintf(os.Stderr, "error: server failed: %v\n", err) + } + }() + + // Wait for context cancellation + <-ctx.Done() + + // Graceful shutdown + fmt.Println("\nShutting down server...") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := server.Shutdown(shutdownCtx); err != nil { + return fmt.Errorf("server shutdown failed: %w", err) + } + + fmt.Println("Server stopped") + return nil +} diff --git a/rigour/cmd/crawler/main.go b/rigour/cmd/crawler/main.go new file mode 100644 index 0000000..f07c521 --- /dev/null +++ b/rigour/cmd/crawler/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net" + "os" + "os/user" + "runtime" + "strings" + "time" + + internalconst "github.com/ctrlsam/rigour/internal" + "github.com/ctrlsam/rigour/internal/messaging" + "github.com/ctrlsam/rigour/internal/messaging/kafka" + "github.com/ctrlsam/rigour/pkg/crawler" + "github.com/ctrlsam/rigour/pkg/crawler/discovery" + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint" + "github.com/ctrlsam/rigour/pkg/types" + "github.com/spf13/cobra" +) + +type cliConfig struct { + fastMode bool + timeout int + useUDP bool + verbose bool + + // Kafka + kafkaBrokers string + + // Discovery settings + scanType string + ports string + topPorts string + retries int + rate int +} + +var ( + config cliConfig + rootCmd = &cobra.Command{ + Use: "rigour [flags]\nTARGET SPECIFICATION:\n\tRequires an ip address or CIDR range\n" + + "EXAMPLES:\n\trigour 192.168.1.0/24\n", + RunE: func(cmd *cobra.Command, args []string) error { + configErr := checkConfig(config) + if configErr != nil { + return configErr + } + + cidrRange := args[0] + ipCount := getCIDRRangeSize(cidrRange) + fmt.Printf("Starting scan of %d IPs in range %s\n", ipCount, cidrRange) + + var producer messaging.Producer[types.Service] + if brokers := strings.TrimSpace(config.kafkaBrokers); brokers != "" { + p, err := kafka.NewTypedProducer[types.Service](kafka.ProducerConfig{ + Brokers: brokers, + Topic: internalconst.KafkaTopicScannedServices, + }) + if err != nil { + return fmt.Errorf("failed to create kafka producer: %w", err) + } + producer = p + defer func() { _ = producer.Close() }() + } + + onEvent := func(ev types.Service) { + // Encode once and reuse for both outputs. + serializedService, err := json.Marshal(ev) + if err != nil { + // Streaming should never abort the whole scan due to a single marshal failure. + fmt.Fprintf(os.Stderr, "failed to marshal event: %v\n", err) + return + } + + // NDJSON output. + _, _ = os.Stdout.Write(append(serializedService, '\n')) + + // Kafka output (optional) + if producer != nil { + // stable partition by IP if present + key := []byte(ev.IP) + if err := producer.Publish(context.Background(), key, ev); err != nil { + fmt.Fprintf(os.Stderr, "failed to publish kafka event: %v\n", err) + } + } + } + + err := crawler.ScanTargetWithDiscoveryStream(cidrRange, createDiscoveryConfig(config), createScanConfig(config), onEvent) + if err != nil { + return fmt.Errorf("Failed running discovery+scan stream (%w)", err) + } + return nil + }, + } +) + +func checkConfig(config cliConfig) error { + if config.useUDP && config.verbose { + user, err := user.Current() + if err != nil { + return fmt.Errorf("Failed to retrieve current user (error: %w)", err) + } + if !((runtime.GOOS == "linux" || runtime.GOOS == "darwin") && user.Uid == "0") { + fmt.Fprintln(os.Stderr, "Note: UDP Scan may require root privileges") + } + } + + return nil +} + +func getCIDRRangeSize(cidr string) int { + _, ipnet, _ := net.ParseCIDR(cidr) + ones, bits := ipnet.Mask.Size() + numIPs := 1 << (bits - ones) + return numIPs +} + +func createScanConfig(config cliConfig) fingerprint.FingerprintConfig { + return fingerprint.FingerprintConfig{ + DefaultTimeout: time.Duration(config.timeout) * time.Millisecond, + FastMode: config.fastMode, + UDP: config.useUDP, + Verbose: config.verbose, + } +} + +func createDiscoveryConfig(config cliConfig) discovery.DiscoveryConfig { + return discovery.DiscoveryConfig{ + ScanType: config.scanType, + Ports: config.ports, + TopPorts: config.topPorts, + Retries: config.retries, + Rate: config.rate, + } +} + +func init() { + rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + + rootCmd.PersistentFlags().BoolVarP(&config.fastMode, "fast", "f", false, "fast mode") + rootCmd.PersistentFlags(). + BoolVarP(&config.useUDP, "udp", "U", false, "run UDP plugins") + + rootCmd.PersistentFlags().BoolVarP(&config.verbose, "verbose", "v", false, "verbose mode") + rootCmd.PersistentFlags(). + IntVarP(&config.timeout, "timeout", "w", 1000, "timeout (milliseconds)") + + // Discovery flags - These control how rigour discovers open ports. + rootCmd.PersistentFlags().StringVar(&config.kafkaBrokers, "kafka-brokers", "localhost:29092", "kafka brokers (comma-separated host:port list); set empty to disable") + rootCmd.PersistentFlags().StringVar(&config.scanType, "scan-type", "c", "discovery scan type (naabu; e.g. c=connect, s=syn)") + rootCmd.PersistentFlags().StringVar(&config.ports, "ports", "", "ports list (e.g. 80,443). If set, overrides top ports") + rootCmd.PersistentFlags().StringVar(&config.topPorts, "top-ports", "1000", "top ports (e.g. 100, 1000, full)") // full + rootCmd.PersistentFlags().IntVar(&config.retries, "retries", 1, "discovery retries") + rootCmd.PersistentFlags().IntVar(&config.rate, "rate", 50_000, "discovery rate (packets per second)") +} + +func main() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/rigour/cmd/persistence/main.go b/rigour/cmd/persistence/main.go new file mode 100644 index 0000000..f41b6a9 --- /dev/null +++ b/rigour/cmd/persistence/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + internalconst "github.com/ctrlsam/rigour/internal" + "github.com/ctrlsam/rigour/pkg/persistence" + "github.com/spf13/cobra" +) + +type cliConfig struct { + brokers string + groupID string + topic string + dbURI string + dbName string + dbCollection string + geoipDataPath string +} + +func main() { + var cfg cliConfig + + root := &cobra.Command{ + Use: "rigour-persistence", + Short: "Consume crawler service events and persist/enrich hosts in MongoDB", + RunE: func(cmd *cobra.Command, args []string) error { + appCfg := persistence.Config{ + KafkaBrokers: cfg.brokers, + KafkaGroupID: cfg.groupID, + Topic: cfg.topic, + DbURI: cfg.dbURI, + DbName: cfg.dbName, + DbCollection: cfg.dbCollection, + DbTimeout: 10 * time.Second, + GeoIPDataDir: cfg.geoipDataPath, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Shutdown on SIGINT/SIGTERM + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + go func() { + <-sigCh + cancel() + }() + + app, err := persistence.NewApp(ctx, appCfg) + if err != nil { + return err + } + defer func() { _ = app.Close(context.Background()) }() + + err = app.Run(ctx) + if err == context.Canceled { + return nil + } + return err + }, + } + + root.Flags().StringVar(&cfg.brokers, "brokers", "localhost:29092", "Kafka brokers (comma-separated)") + root.Flags().StringVar(&cfg.groupID, "group", "rigour-persistence", "Kafka consumer group id") + root.Flags().StringVar(&cfg.topic, "topic", internalconst.KafkaTopicScannedServices, "Kafka topic to consume") + + root.Flags().StringVar(&cfg.dbURI, "mongo-uri", "mongodb://localhost:27017", "MongoDB connection URI") + root.Flags().StringVar(&cfg.dbName, "mongo-db", internalconst.DatabaseName, "MongoDB database name") + root.Flags().StringVar(&cfg.dbCollection, "mongo-coll", internalconst.HostsRepositoryName, "MongoDB hosts collection name") + + root.Flags().StringVar(&cfg.geoipDataPath, "geoip-path", "", "Path to GeoIP data directory containing GeoLite2 database files") + + if err := root.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/rigour/common/common/config.py b/rigour/common/common/config.py deleted file mode 100644 index e779e8c..0000000 --- a/rigour/common/common/config.py +++ /dev/null @@ -1,27 +0,0 @@ -import os - - -class Config: - @staticmethod - def get_mongo_uri(default: str = "mongodb://localhost:27017") -> str: - return os.environ.get("MONGO_URL", default) - - @staticmethod - def get_mongo_db(default: str = "rigour") -> str: - return os.environ.get("MONGO_DB", default) - - @staticmethod - def get_rabbitmq_uri(default: str = "amqp://localhost:5672/") -> str: - return os.environ.get("RABBITMQ_URL", default) - - @staticmethod - def get_networks(default: str = "10.0.0.0/8") -> str: - return os.environ.get("NETWORKS", default) - - @staticmethod - def get_ports(default: str = "80") -> str: - return os.environ.get("PORTS", default) - - @staticmethod - def get_scan_collection() -> str: - return "scans" diff --git a/rigour/common/common/database/mongodb.py b/rigour/common/common/database/mongodb.py deleted file mode 100644 index b0fcff8..0000000 --- a/rigour/common/common/database/mongodb.py +++ /dev/null @@ -1,12 +0,0 @@ -from common.config import Config -from pymongo import MongoClient - - -class Database: - def __init__(self, uri: str | None = None, db: str | None = None): - self.client = MongoClient(uri or Config.get_mongo_uri()) - self.db = self.client[db or Config.get_mongo_db()] - self.scans = self.db[Config.get_scan_collection()] - - def close(self): - self.client.close() diff --git a/rigour/common/common/queue/rabbitmq.py b/rigour/common/common/queue/rabbitmq.py deleted file mode 100644 index 62e9dee..0000000 --- a/rigour/common/common/queue/rabbitmq.py +++ /dev/null @@ -1,76 +0,0 @@ -import json -import threading -from typing import Callable - -import pika -from common.config import Config -from common.utils import DateTimeEncoder -from loguru import logger - - -class RabbitMQQueueManager: - def __init__(self, uri: str | None = None, exchange="data_exchange"): - self.uri = uri or Config.get_rabbitmq_uri() - self.exchange = exchange - - # Create a thread-local storage for the connection and channel - # This prevents thread errors when using the same connection in multiple threads - self.local = threading.local() - - def connect(self): - self.local.connection = pika.BlockingConnection(pika.URLParameters(self.uri)) - self.local.channel = self.local.connection.channel() - self.local.channel.exchange_declare( - exchange=self.exchange, exchange_type="topic" - ) - logger.info( - f"Thread {threading.current_thread().name}: Connected to RabbitMQ on\ - {self.uri}, exchange '{self.exchange}' declared." - ) - - def _get_channel(self): - """Get or create a channel for the current thread.""" - if not hasattr(self.local, "channel"): - self.connect() - return self.local.channel - - def publish(self, routing_key: str, message: dict): - """Publish a message to the specified routing key.""" - channel = self._get_channel() - channel.basic_publish( - exchange=self.exchange, - routing_key=routing_key, - body=json.dumps(message, cls=DateTimeEncoder), - ) - logger.debug(f"Published message to '{routing_key}': {message}") - - def consume(self, routing_key: str, callback: Callable): - """Consume messages from the specified routing key pattern.""" - channel = self._get_channel() - result = channel.queue_declare("", exclusive=True) - queue_name = result.method.queue - - channel.queue_bind( - exchange=self.exchange, queue=queue_name, routing_key=routing_key - ) - logger.info(f"Subscribed to '{routing_key}' with queue '{queue_name}'.") - - def on_message(channel, method, properties, body): - message = json.loads(body) - logger.debug(f"Received message from '{method.routing_key}': {message}") - callback(message) - channel.basic_ack(delivery_tag=method.delivery_tag) - - channel.basic_consume(queue=queue_name, on_message_callback=on_message) - logger.info("Starting consumption...") - try: - channel.start_consuming() - except Exception as e: - logger.exception(f"Error consuming messages: {e}") - self.close() - - def close(self): - """Close the connection to the RabbitMQ server.""" - if hasattr(self.local, "connection") and not self.local.connection.is_closed: - self.local.connection.close() - logger.info("Connection to RabbitMQ closed.") diff --git a/rigour/common/common/queue/rabbitmq_asyncio.py b/rigour/common/common/queue/rabbitmq_asyncio.py deleted file mode 100644 index 9fb08a5..0000000 --- a/rigour/common/common/queue/rabbitmq_asyncio.py +++ /dev/null @@ -1,73 +0,0 @@ -from datetime import datetime -from typing import Callable - -import aiormq -import aiormq.types -import msgpack -from common.config import Config -from loguru import logger - - -def decode_datetime(obj): - if "__datetime__" in obj: - obj = datetime.strptime(obj["as_str"], "%Y%m%dT%H:%M:%S.%f") - return obj - - -def encode_datetime(obj): - if isinstance(obj, datetime): - return {"__datetime__": True, "as_str": obj.strftime("%Y%m%dT%H:%M:%S.%f")} - return obj - - -class AsyncRabbitMQQueueManager: - def __init__(self, uri: str | None = None, exchange: str = "data_exchange"): - self.uri = uri or Config.get_rabbitmq_uri() - self.exchange = exchange - self.connection = None - self.channel = None - - async def connect(self): - self.connection = await aiormq.connect(self.uri) - self.channel = await self.connection.channel() - await self.channel.exchange_declare( - exchange=self.exchange, exchange_type="topic" - ) - - async def get_channel(self) -> aiormq.types.AbstractChannel: - """Get or create a channel for the current thread.""" - if not self.channel: - await self.connect() - assert self.channel, "Connection not established." - return self.channel - - async def publish(self, routing_key: str, message: dict) -> None: - """Publish a message to the specified routing key.""" - channel = await self.get_channel() - - logger.debug(f"Publishing message to '{routing_key}': {message}") - body: bytes = msgpack.packb(message, default=encode_datetime) - await channel.basic_publish( - exchange=self.exchange, - routing_key=routing_key, - body=body, - ) - - async def consume(self, routing_key: str, callback: Callable) -> None: - """Consume messages from the specified routing key pattern.""" - channel = await self.get_channel() - - result = await channel.queue_declare("", exclusive=True) - queue_name = result.queue - - assert queue_name, "Queue name not found." - logger.info(f"Subscribing to '{routing_key}' with queue '{queue_name}'.") - await channel.queue_bind( - exchange=self.exchange, queue=queue_name, routing_key=routing_key - ) - - async def on_message(message: aiormq.types.DeliveredMessage): - message = msgpack.unpackb(message.body, object_hook=decode_datetime) - await callback(message) - - await self.channel.basic_consume(queue=queue_name, consumer_callback=on_message) diff --git a/rigour/common/common/subprocess.py b/rigour/common/common/subprocess.py deleted file mode 100644 index 6dab715..0000000 --- a/rigour/common/common/subprocess.py +++ /dev/null @@ -1,144 +0,0 @@ -import asyncio -import json -from typing import Generic, TypeVar - -from loguru import logger - -T = TypeVar("T") - - -class AsyncSubprocessBase(Generic[T]): - def __init__(self, command, enable_piping: bool = False): - self.command = command - self.process: asyncio.subprocess.Process | None = None - self._stdin_lock = asyncio.Lock() - self._enable_piping = enable_piping - self._stdout_task = None - self._stderr_task = None - - async def run(self, callback: callable): - """Run the subprocess asynchronously and process output line-by-line.""" - args = self.command.build() - - # Start the subprocess with appropriate pipes - logger.debug(f"Starting subprocess with args: {' '.join(args)}") - self.process = await self._create_subprocess(args) - - # Create asynchronous tasks for reading stdout and stderr - self._stdout_task = asyncio.create_task(self._read_stdout(callback)) - self._stderr_task = asyncio.create_task(self._read_stderr()) - - if not self._enable_piping: - # If piping is not enabled, wait for process to complete - await self.process.wait() - await asyncio.gather(self._stdout_task, self._stderr_task) - - return_code = self.process.returncode - if return_code != 0: - logger.warning(f"Subprocess exited with return code {return_code}") - - async def _create_subprocess(self, args): - """Create subprocess with appropriate pipes.""" - stdin_pipe = asyncio.subprocess.PIPE if self._enable_piping else None - return await asyncio.create_subprocess_exec( - *args, - stdin=stdin_pipe, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - async def _read_stderr(self): - """Asynchronously reads lines from the subprocess's stderr.""" - if not self.process or not self.process.stderr: - return - - try: - async for line in self.process.stderr: - line = line.strip() - if line: - logger.error(f"Subprocess stderr: {line.decode()}") - except Exception as e: - logger.error(f"Error reading stderr: {e}") - - async def _read_stdout(self, callback: callable): - """Reads the output from the subprocess asynchronously, handling large lines.""" - if not self.process or not self.process.stdout: - return - - buffer = b"" - try: - while True: - chunk = await self.process.stdout.read(4096) # Read in 4KB chunks - if not chunk: - break - buffer += chunk - while b"\n" in buffer: - line, buffer = buffer.split(b"\n", 1) - await self._process_line(line, callback) - - # Process any remaining data in the buffer - if buffer: - await self._process_line(buffer, callback) - except Exception as e: - logger.exception(f"Error reading stdout: {e}") - - async def _process_line(self, line: bytes, callback: callable): - """Processes a single line from stdout.""" - line = line.strip() - if not line: - return - - try: - result = json.loads(line) - parsed_result = await self._parse_result(result) - if parsed_result: - await callback(parsed_result) - except json.JSONDecodeError: - logger.error(f"Failed to parse JSON: {line}") - except Exception as e: - logger.exception(f"Error processing line: {e}") - - async def _parse_result(self, result: dict) -> T: - """Parse the raw result into appropriate type. Must be implemented by subclasses.""" - raise NotImplementedError - - async def pipe(self, data: str): - """Pipe input data into the subprocess asynchronously.""" - if not self._enable_piping: - logger.error("Piping is not enabled for this subprocess") - return - - if not self.process or not self.process.stdin: - logger.error("Cannot pipe: process or stdin not available") - return - - try: - async with self._stdin_lock: - self.process.stdin.write(f"{data}\n".encode()) - await self.process.stdin.drain() - logger.debug(f"Successfully piped data to subprocess: {data}") - except Exception as e: - logger.error(f"Error piping data to subprocess: {e}") - - async def close(self): - """Close the process and clean up resources.""" - if self.process: - try: - # Close stdin first if piping was enabled - if self._enable_piping and self.process.stdin: - self.process.stdin.close() - await self.process.stdin.wait_closed() - - # Cancel reading tasks - if self._stdout_task: - self._stdout_task.cancel() - if self._stderr_task: - self._stderr_task.cancel() - - # Terminate the process - self.process.terminate() - await self.process.wait() - - logger.info("Subprocess and tasks terminated.") - except Exception as e: - logger.error(f"Error closing subprocess: {e}") diff --git a/rigour/common/common/types.py b/rigour/common/common/types.py deleted file mode 100644 index b4ddc5f..0000000 --- a/rigour/common/common/types.py +++ /dev/null @@ -1,57 +0,0 @@ -import datetime -from dataclasses import dataclass - -from pydantic import BaseModel - - -@dataclass -class Vulnerability: - name: str - title: str - version: str - link: str - # TODO: This should be linked to the service and port - - -@dataclass -class Location: - country_code: str | None = None - continent_name: str | None = None - country_name: str | None = None - accuracy_radius: int | None = None - latitude: float | None = None - longitude: float | None = None - - -@dataclass -class Banner: - service: str - port: int | None - data: dict - - -@dataclass -class Host: - location: Location - banner: Banner | None = None - vulnerabilities: list[Vulnerability] | None = None - - -@dataclass -class HostMessage: - ip: str - port: int - host: Host - - -class DBHost(BaseModel): - ip: str - location: Location - banners: dict[str, Banner] = {} - vulnerabilities: list[Vulnerability] = [] - updated_at: datetime.datetime - first_seen: datetime.datetime - - class Config: - from_attributes = True # Allows compatibility with ORM objects - extra = "ignore" # Ignores extra fields not defined in the model diff --git a/rigour/common/common/utils.py b/rigour/common/common/utils.py deleted file mode 100644 index 96bf879..0000000 --- a/rigour/common/common/utils.py +++ /dev/null @@ -1,62 +0,0 @@ -import json -from dataclasses import asdict -from datetime import datetime - -from common.database.mongodb import Database -from common.types import HostMessage -from loguru import logger - - -class DateTimeEncoder(json.JSONEncoder): - def default(self, obj): # type: ignore - if isinstance(obj, datetime): - return obj.isoformat() - return super().default(obj) - - -def route_key_from_host_message(message: HostMessage, data_type: str) -> str: - return ( - f"{message.host.location.country_code}.{message.port}.{message.ip}.{data_type}" - ) - - -# TODO: move this elsewhere -def save_banner(db: Database, message: HostMessage) -> None: - assert message.host.banner is not None - logger.debug( - f"Saving banner to database for IP: {message.ip}, Port: {message.port}" - ) - - now = datetime.now() - - db.scans.update_one( - {"ip": message.ip}, - { - "$set": { - f"banners.{message.host.banner.service}": asdict(message.host.banner), - "updated_at": now, - } - }, - upsert=True, - ) - - -# TODO: move this elsewhere -def save_vulnerability(db: Database, message: HostMessage) -> None: - assert message.host.vulnerabilities is not None - logger.debug( - f"Saving vulnerabilities to database for IP: {message.ip}, Port: {message.port}" - ) - - now = datetime.now() - - db.scans.update_one( - {"ip": message.ip}, - { - "$set": { - "vulnerabilities": [asdict(v) for v in message.host.vulnerabilities], - "updated_at": now, - } - }, - upsert=True, - ) diff --git a/rigour/common/pyproject.toml b/rigour/common/pyproject.toml deleted file mode 100644 index 2a4fd31..0000000 --- a/rigour/common/pyproject.toml +++ /dev/null @@ -1,34 +0,0 @@ -[project] -name = "common" -version = "0.0.1" -description = "Common code for the Rigour project" -readme = "README.md" -classifiers = [ - "License :: Other/Proprietary License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", -] -requires-python = "~=3.11" -dependencies = [ - "pymongo", - "loguru", - "pika", - "dacite", - "pydantic", - "aiormq", - "msgpack" -] - -[project.optional-dependencies] -dev = [ - -] - -[tool.setuptools.packages.find] -include = ["common*"] - -[build-system] -requires = ["setuptools", "setuptools-scm"] -build-backend = "setuptools.build_meta" diff --git a/rigour/go.mod b/rigour/go.mod new file mode 100644 index 0000000..2ec3b5c --- /dev/null +++ b/rigour/go.mod @@ -0,0 +1,174 @@ +module github.com/ctrlsam/rigour + +go 1.24.0 + +require ( + github.com/go-chi/chi/v5 v5.2.3 + github.com/go-chi/render v1.0.3 + github.com/ory/dockertest/v3 v3.9.1 + github.com/projectdiscovery/goflags v0.1.74 + github.com/projectdiscovery/naabu/v2 v2.3.7 + github.com/projectdiscovery/wappalyzergo v0.2.17 + github.com/segmentio/kafka-go v0.4.48 + github.com/spf13/cobra v1.5.0 + github.com/stretchr/testify v1.11.1 + go.mongodb.org/mongo-driver v1.17.4 + golang.org/x/crypto v0.45.0 + golang.org/x/term v0.37.0 +) + +require ( + aead.dev/minisign v0.2.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect + github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect + github.com/Ullaakut/nmap/v3 v3.0.6 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/akrylysov/pogreb v0.10.1 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/charmbracelet/glamour v0.8.0 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.3.2 // indirect + github.com/cheggaaa/pb/v3 v3.1.4 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/docker/cli v20.10.17+incompatible // indirect + github.com/docker/docker v20.10.17+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/gaissmai/bart v0.26.0 // indirect + github.com/go-chi/cors v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-github/v30 v30.1.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/gopacket/gopacket v1.2.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mholt/archives v0.1.0 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/miekg/dns v1.1.62 // indirect + github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/nwaples/rardecode/v2 v2.2.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.3 // indirect + github.com/oschwald/geoip2-golang v1.13.0 // indirect + github.com/oschwald/maxminddb-golang v1.13.0 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/projectdiscovery/asnmap v1.1.1 // indirect + github.com/projectdiscovery/blackrock v0.0.1 // indirect + github.com/projectdiscovery/cdncheck v1.2.10 // indirect + github.com/projectdiscovery/clistats v0.1.1 // indirect + github.com/projectdiscovery/dnsx v1.2.2 // indirect + github.com/projectdiscovery/fastdialer v0.4.16 // indirect + github.com/projectdiscovery/freeport v0.0.7 // indirect + github.com/projectdiscovery/gologger v1.1.60 // indirect + github.com/projectdiscovery/hmap v0.0.95 // indirect + github.com/projectdiscovery/ipranger v0.0.53 // indirect + github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 // indirect + github.com/projectdiscovery/mapcidr v1.1.97 // indirect + github.com/projectdiscovery/networkpolicy v0.1.28 // indirect + github.com/projectdiscovery/ratelimit v0.0.82 // indirect + github.com/projectdiscovery/retryabledns v1.0.108 // indirect + github.com/projectdiscovery/retryablehttp-go v1.0.131 // indirect + github.com/projectdiscovery/uncover v1.1.0 // indirect + github.com/projectdiscovery/utils v0.7.0 // indirect + github.com/refraction-networking/utls v1.7.1 // indirect + github.com/remeh/sizedwaitgroup v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect + github.com/shirou/gopsutil/v3 v3.23.7 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/sorairolake/lzip-go v0.3.5 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/tidwall/btree v1.6.0 // indirect + github.com/tidwall/buntdb v1.3.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/grect v0.1.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/rtred v0.1.2 // indirect + github.com/tidwall/tinyqueue v0.1.1 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/weppos/publicsuffix-go v0.40.3-0.20250408071509-6074bbe7fd39 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/yl2chen/cidranger v1.0.2 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yuin/goldmark v1.7.4 // indirect + github.com/yuin/goldmark-emoji v1.0.3 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zcalusic/sysinfo v1.0.2 // indirect + github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect + github.com/zmap/zcrypto v0.0.0-20230814193918-dbe676986518 // indirect + go.etcd.io/bbolt v1.3.7 // indirect + go.uber.org/multierr v1.11.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.38.0 // indirect + gopkg.in/djherbis/times.v1 v1.3.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/rigour/go.sum b/rigour/go.sum new file mode 100644 index 0000000..5511000 --- /dev/null +++ b/rigour/go.sum @@ -0,0 +1,791 @@ +aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk= +aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub47e7kd2PLZeACxc1LkiiNoDOFRClE= +github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2pivrPICvLOuROKmlqURtFIEsoJZaMidQfCG1+D4= +github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8= +github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/Ullaakut/nmap/v3 v3.0.6 h1:ZCQ70TQp97f/YqIFhlzFMDi5xVDeA0CwMbNeJZGA//A= +github.com/Ullaakut/nmap/v3 v3.0.6/go.mod h1:dd5K68P7LHc5nKrFwQx6EdTt61O9UN5x3zn1R4SLcco= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w= +github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY= +github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= +github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= +github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo= +github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ= +github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gaissmai/bart v0.26.0 h1:xOZ57E9hJLBiQaSyeZa9wgWhGuzfGACgqp4BE77OkO0= +github.com/gaissmai/bart v0.26.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= +github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo= +github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8= +github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopacket/gopacket v1.2.0 h1:eXbzFad7f73P1n2EJHQlsKuvIMJjVXK5tXoSca78I3A= +github.com/gopacket/gopacket v1.2.0/go.mod h1:BrAKEy5EOGQ76LSqh7DMAr7z0NNPdczWm2GxCG7+I8M= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW7GN5ngLm8YUZIPzf394= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q= +github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 h1:yRZGarbxsRytL6EGgbqK2mCY+Lk5MWKQYKJT2gEglhc= +github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= +github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= +github.com/opencontainers/runc v1.1.3/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= +github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= +github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= +github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= +github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= +github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kID2iwsDqI= +github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60= +github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= +github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= +github.com/projectdiscovery/cdncheck v1.2.10 h1:Ox86LS8RFjq6pYNTP3Eqdawlor/h+bnb7BTEKBpzFyM= +github.com/projectdiscovery/cdncheck v1.2.10/go.mod h1:ibL9HoZs2JYTEUBOZo4f+W+XEzQifFLOf4bpgFStgj4= +github.com/projectdiscovery/clistats v0.1.1 h1:8mwbdbwTU4aT88TJvwIzTpiNeow3XnAB72JIg66c8wE= +github.com/projectdiscovery/clistats v0.1.1/go.mod h1:4LtTC9Oy//RiuT1+76MfTg8Hqs7FQp1JIGBM3nHK6a0= +github.com/projectdiscovery/dnsx v1.2.2 h1:ZjUov0GOyrS8ERlKAAhk+AOkqzaYHBzCP0qZfO+6Ihg= +github.com/projectdiscovery/dnsx v1.2.2/go.mod h1:3iYm86OEqo0WxeGDkVl5WZNmG0qYE5TYNx8fBg6wX1I= +github.com/projectdiscovery/fastdialer v0.4.16 h1:rmCNr5N/9KTm0nSYjSuQ5j3aXmNIPf6HhJlAhN/7NRI= +github.com/projectdiscovery/fastdialer v0.4.16/go.mod h1:X0l4+KqOE/aIL00pyTnBj4pWQDPYnCGL7cwZsJu6SCQ= +github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk= +github.com/projectdiscovery/freeport v0.0.7/go.mod h1:cOhWKvNBe9xM6dFJ3RrrLvJ5vXx2NQ36SecuwjenV2k= +github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c= +github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4= +github.com/projectdiscovery/gologger v1.1.60 h1:N2Zyu4WA2RgUeqSAdfhv/CLS4de8lDDc2+IdLKcAd5U= +github.com/projectdiscovery/gologger v1.1.60/go.mod h1:8FJFKmo0N4ITIH3n1Jy4ze6ijr+mA3t78g+VpN8uBRU= +github.com/projectdiscovery/hmap v0.0.95 h1:OO6MCySlK2xMzvJmsYUwdaI7YWv/U437OtsN0Ovw72k= +github.com/projectdiscovery/hmap v0.0.95/go.mod h1:KiTRdGd/GzX7uaoFWPrPBxPf4X/uZ9HTQ9dQ8x7x1bo= +github.com/projectdiscovery/ipranger v0.0.53 h1:gb4yEqtC2MJl1tSdx/ycao1A1wl7sHqjHeifZidO3Z4= +github.com/projectdiscovery/ipranger v0.0.53/go.mod h1:r6R0DFKQRo4QR2zjZXqLRCp0ovbco8F/NmOI+pK4db8= +github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 h1:ZScLodGSezQVwsQDtBSMFp72WDq0nNN+KE/5DHKY5QE= +github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983/go.mod h1:3G3BRKui7nMuDFAZKR/M2hiOLtaOmyukT20g88qRQjI= +github.com/projectdiscovery/mapcidr v1.1.97 h1:7FkxNNVXp+m1rIu5Nv/2SrF9k4+LwP8QuWs2puwy+2w= +github.com/projectdiscovery/mapcidr v1.1.97/go.mod h1:9dgTJh1SP02gYZdpzMjm6vtYFkEHQHoTyaVNvaeJ7lA= +github.com/projectdiscovery/naabu/v2 v2.3.7 h1:DFADMDWgaSDRBEOVZtZZ8DTb7ugewjSqptDlij43uBs= +github.com/projectdiscovery/naabu/v2 v2.3.7/go.mod h1:GsQWK3EzLla0+a+sMczqyswK4tb0ya7l5DIbZbRtV4c= +github.com/projectdiscovery/networkpolicy v0.1.28 h1:Rwg8iZmM4n+CRWyUClthaSrTqDAW8zBI2HULRO1CF3k= +github.com/projectdiscovery/networkpolicy v0.1.28/go.mod h1:/3XfgnxKNuxaTZc6wZ/Pq6fiKvK8N4OQyLmfcUeDk2E= +github.com/projectdiscovery/ratelimit v0.0.82 h1:rtO5SQf5uQFu5zTahTaTcO06OxmG8EIF1qhdFPIyTak= +github.com/projectdiscovery/ratelimit v0.0.82/go.mod h1:z076BrLkBb5yS7uhHNoCTf8X/BvFSGRxwQ8EzEL9afM= +github.com/projectdiscovery/retryabledns v1.0.108 h1:47LYRW2LY/0cDnZQfUhoOHNxe9rNc9NQ9ZfNrV/GbyM= +github.com/projectdiscovery/retryabledns v1.0.108/go.mod h1:j7H7K6JZePh9PeNleeRUtDSrkUKMpwDhZw3Ogewzio8= +github.com/projectdiscovery/retryablehttp-go v1.0.131 h1:OU2x9fVDIWnDoKvT8tKbaCONTL1gHnTOIFQFXmnEOE0= +github.com/projectdiscovery/retryablehttp-go v1.0.131/go.mod h1:ttW+Zka1L8IwEUhJ4zArbC+pKZum7b47fzV+4VGN6cA= +github.com/projectdiscovery/uncover v1.1.0 h1:UDp/qLZn78YZb6VPoOrfyP1vz+ojEx8VrTTyjjRt9UU= +github.com/projectdiscovery/uncover v1.1.0/go.mod h1:2rXINmMe/lmVAt2jn9CpAOs9An57/JEeLZobY3Z9kUs= +github.com/projectdiscovery/utils v0.7.0 h1:akjTW9tt2QTv3BCiXlqSf7OfodrqrcaIQowuF7H1BWw= +github.com/projectdiscovery/utils v0.7.0/go.mod h1:j4Fb6PDir9PcTxLOL9cpSVDPVKtLTZwdVxxMAeG0JjA= +github.com/projectdiscovery/wappalyzergo v0.2.17 h1:pYRBRJhR0Wuvx6O08DuicDZVltq1kbSlI5xr8nHXq/0= +github.com/projectdiscovery/wappalyzergo v0.2.17/go.mod h1:F8X79ljvmvrG+EIxdxWS9VbdkVTsQupHYz+kXlp8O0o= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/refraction-networking/utls v1.7.1 h1:dxg+jla3uocgN8HtX+ccwDr68uCBBO3qLrkZUbqkcw0= +github.com/refraction-networking/utls v1.7.1/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ= +github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= +github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/segmentio/kafka-go v0.4.48 h1:9jyu9CWK4W5W+SroCe8EffbrRZVqAOkuaLd/ApID4Vs= +github.com/segmentio/kafka-go v0.4.48/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg= +github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= +github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= +github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= +github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= +github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA= +github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= +github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= +github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= +github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= +github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= +github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.30.2-0.20230730094716-a20f9abcc222/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ= +github.com/weppos/publicsuffix-go v0.40.3-0.20250408071509-6074bbe7fd39 h1:Bz/zVM/LoGZ9IztGBHrq2zlFQQbEG8dBYnxb4hamIHM= +github.com/weppos/publicsuffix-go v0.40.3-0.20250408071509-6074bbe7fd39/go.mod h1:2oFzEwGYI7lhiqG0YkkcKa6VcpjVinQbWxaPzytDmLA= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= +github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zcalusic/sysinfo v1.0.2 h1:nwTTo2a+WQ0NXwo0BGRojOJvJ/5XKvQih+2RrtWqfxc= +github.com/zcalusic/sysinfo v1.0.2/go.mod h1:kluzTYflRWo6/tXVMJPdEjShsbPpsFRyy+p1mBQPC30= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 h1:Nzukz5fNOBIHOsnP+6I79kPx3QhLv8nBy2mfFhBRq30= +github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= +github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20230814193918-dbe676986518 h1:O8GHQBxrphDuNhJQdKBHwP3JQUtZUyi3b+jjPYmF7oA= +github.com/zmap/zcrypto v0.0.0-20230814193918-dbe676986518/go.mod h1:Z2SNNuFhO+AAsezbGEHTWeW30hHv5niUYT3fwJ61Nl0= +github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= +go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o= +gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= +gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/rigour/internal/api/handlers.go b/rigour/internal/api/handlers.go new file mode 100644 index 0000000..697d6dc --- /dev/null +++ b/rigour/internal/api/handlers.go @@ -0,0 +1,145 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/ctrlsam/rigour/internal/storage" + apimodels "github.com/ctrlsam/rigour/pkg/api" + "github.com/go-chi/render" +) + +// Handler provides HTTP handler methods for the API. +type Handler struct { + queryHandler *QueryHandler +} + +// NewHandler creates a new API handler. +func NewHandler(repository storage.HostRepository) *Handler { + return &Handler{ + queryHandler: NewQueryHandler(repository), + } +} + +// SearchHandler handles GET /api/hosts/search requests with query parameters. +func (handler *Handler) SearchHandler(w http.ResponseWriter, r *http.Request) { + // Parse filter from query parameter if provided + filter := make(map[string]interface{}) + filterParam := r.URL.Query().Get("filter") + if filterParam != "" { + if err := json.Unmarshal([]byte(filterParam), &filter); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, map[string]string{"error": "Invalid filter parameter"}) + return + } + } + + // Parse limit from query parameter + limit := apimodels.DefaultPageSize + if limitParam := r.URL.Query().Get("limit"); limitParam != "" { + var parsed int + if _, err := fmt.Sscanf(limitParam, "%d", &parsed); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, map[string]string{"error": "Invalid limit parameter"}) + return + } + limit = apimodels.ValidatePageSize(parsed) + } + + // Parse pagination token if provided + var lastID string + if pageToken := r.URL.Query().Get("page_token"); pageToken != "" { + token, err := apimodels.DecodePaginationToken(pageToken) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, map[string]string{"error": "Invalid page token"}) + return + } + if token != nil { + lastID = token.LastID + } + } + + // Execute search + hosts, nextID, err := handler.queryHandler.Search(r.Context(), filter, lastID, limit) + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, map[string]string{"error": "Search failed: " + err.Error()}) + return + } + + // Build response + resp := apimodels.SearchResponse{ + Hosts: hosts, + } + + // Generate next page token if there are more results + if nextID != "" { + token, err := apimodels.EncodePaginationToken(nextID, "_id") + if err == nil { + resp.NextPageToken = token + } + } + + // Return response + render.JSON(w, r, resp) +} + +// GetHostHandler handles GET /api/hosts/{ip} requests. +func (handler *Handler) GetHostHandler(w http.ResponseWriter, r *http.Request) { + ip := r.PathValue("ip") + if ip == "" { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, map[string]string{"error": "IP address is required"}) + return + } + + host, err := handler.queryHandler.GetByIP(r.Context(), ip) + if err != nil { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, map[string]string{"error": err.Error()}) + return + } + + render.JSON(w, r, host) +} + +// FacetsHandler handles GET /api/facets requests. +func (handler *Handler) FacetsHandler(w http.ResponseWriter, r *http.Request) { + // Parse filter from query parameter if provided + filter := make(map[string]interface{}) + filterParam := r.URL.Query().Get("filter") + if filterParam != "" { + if err := json.Unmarshal([]byte(filterParam), &filter); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, map[string]string{"error": "Invalid filter parameter"}) + return + } + } + + // Execute facet aggregation + agg, err := handler.queryHandler.Facets(r.Context(), filter) + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, map[string]string{"error": "Facet aggregation failed: " + err.Error()}) + return + } + + // Build response + resp := apimodels.FacetResponse{ + Facets: apimodels.FacetCounts{ + Services: agg.Services, + Countries: agg.Countries, + ASNs: agg.ASNs, + }, + } + + // Return response + render.JSON(w, r, resp) +} + +// HealthHandler handles GET /health requests. +func (handler *Handler) HealthHandler(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, map[string]string{"status": "ok"}) +} diff --git a/rigour/internal/api/query.go b/rigour/internal/api/query.go new file mode 100644 index 0000000..39481ab --- /dev/null +++ b/rigour/internal/api/query.go @@ -0,0 +1,35 @@ +package api + +import ( + "context" + + "github.com/ctrlsam/rigour/internal/storage" + "github.com/ctrlsam/rigour/pkg/types" +) + +// QueryHandler provides methods for executing queries using the storage abstraction. +type QueryHandler struct { + repository storage.HostRepository +} + +// NewQueryHandler creates a new QueryHandler. +func NewQueryHandler(repository storage.HostRepository) *QueryHandler { + return &QueryHandler{ + repository: repository, + } +} + +// Search queries hosts with filter and pagination support. +func (qh *QueryHandler) Search(ctx context.Context, filter map[string]interface{}, lastID string, limit int) ([]types.Host, string, error) { + return qh.repository.Search(ctx, filter, lastID, limit) +} + +// GetByIP retrieves a single host by IP address. +func (qh *QueryHandler) GetByIP(ctx context.Context, ip string) (*types.Host, error) { + return qh.repository.GetByIP(ctx, ip) +} + +// Facets performs aggregation for facet counts. +func (qh *QueryHandler) Facets(ctx context.Context, filter map[string]interface{}) (*storage.FacetCounts, error) { + return qh.repository.Facets(ctx, filter) +} diff --git a/rigour/internal/api/router.go b/rigour/internal/api/router.go new file mode 100644 index 0000000..79d4d10 --- /dev/null +++ b/rigour/internal/api/router.go @@ -0,0 +1,55 @@ +package api + +import ( + "github.com/ctrlsam/rigour/internal/storage" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" +) + +// Router provides the Chi router configuration for the API. +type Router struct { + router *chi.Mux + handler *Handler +} + +// NewRouter creates a new API router. +func NewRouter(repository storage.HostRepository) *Router { + r := chi.NewRouter() + + // Add CORS middleware + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, + })) + + // Add other middleware + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(middleware.RequestID) + + handler := NewHandler(repository) + + // Register routes + r.Get("/health", handler.HealthHandler) + + r.Route("/api", func(r chi.Router) { + r.Get("/hosts/search", handler.SearchHandler) + r.Get("/hosts/{ip}", handler.GetHostHandler) + r.Get("/facets", handler.FacetsHandler) + }) + + return &Router{ + router: r, + handler: handler, + } +} + +// Handler returns the underlying Chi router for use with http.ListenAndServe. +func (r *Router) Handler() *chi.Mux { + return r.router +} diff --git a/rigour/internal/constants.go b/rigour/internal/constants.go new file mode 100644 index 0000000..a1d64fe --- /dev/null +++ b/rigour/internal/constants.go @@ -0,0 +1,10 @@ +package internal + +const ( + // Database Constants + DatabaseName = "rigour" + HostsRepositoryName = "hosts" + + // Kafka Constants + KafkaTopicScannedServices = "rigour.crawler.service" +) diff --git a/rigour/internal/messaging/codec.go b/rigour/internal/messaging/codec.go new file mode 100644 index 0000000..27e4028 --- /dev/null +++ b/rigour/internal/messaging/codec.go @@ -0,0 +1,27 @@ +package messaging + +import ( + "encoding/json" +) + +// Codec encodes/decodes values to/from bytes. +// +// This keeps serialization decisions out of transport providers. +type Codec[T any] interface { + Marshal(value T) ([]byte, error) + Unmarshal(data []byte, out *T) error +} + +// JSONCodec is a convenience codec for JSON serialization. +type JSONCodec[T any] struct{} + +func (JSONCodec[T]) Marshal(value T) ([]byte, error) { return json.Marshal(value) } +func (JSONCodec[T]) Unmarshal(data []byte, out *T) error { + return json.Unmarshal(data, out) +} + +type TypedMessage[T any] struct { + Key []byte + Value T + Raw []byte +} diff --git a/rigour/internal/messaging/kafka/consumer.go b/rigour/internal/messaging/kafka/consumer.go new file mode 100644 index 0000000..2289a07 --- /dev/null +++ b/rigour/internal/messaging/kafka/consumer.go @@ -0,0 +1,114 @@ +package kafka + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/ctrlsam/rigour/internal/messaging" + + kafka "github.com/segmentio/kafka-go" +) + +type ConsumerConfig struct { + Brokers string // Comma-separated list of broker addresses + Topic string + GroupID string +} + +type Consumer struct { + reader *kafka.Reader +} + +func (c ConsumerConfig) Validate() error { + if len(c.Brokers) == 0 { + return errors.New("kafka: brokers is empty") + } + if strings.TrimSpace(c.Topic) == "" { + return errors.New("kafka: topic is empty") + } + if strings.TrimSpace(c.GroupID) == "" { + return errors.New("kafka: group id is empty") + } + return nil +} + +func NewConsumer(cfg ConsumerConfig) (*Consumer, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + + brokers := strings.Split(cfg.Brokers, ",") + for i := range brokers { + brokers[i] = strings.TrimSpace(brokers[i]) + } + + reader := kafka.NewReader(kafka.ReaderConfig{ + Brokers: brokers, + Topic: cfg.Topic, + GroupID: cfg.GroupID, + MinBytes: 1, + MaxBytes: 10e6, + CommitInterval: 1 * time.Second, // automatic commits every second + }) + + return &Consumer{reader: reader}, nil +} + +func (c *Consumer) Close() error { + if c == nil || c.reader == nil { + return nil + } + return c.reader.Close() +} + +// TypedConsumer[T] wraps a Consumer and automatically unmarshals messages to type T. +type TypedConsumer[T any] struct { + consumer *Consumer + codec messaging.Codec[T] +} + +var _ messaging.Consumer[any] = (*TypedConsumer[any])(nil) + +// NewTypedConsumer creates a new consumer that parses messages to type T. +func NewTypedConsumer[T any](cfg ConsumerConfig) (*TypedConsumer[T], error) { + consumer, err := NewConsumer(cfg) + if err != nil { + return nil, err + } + return &TypedConsumer[T]{ + consumer: consumer, + codec: messaging.JSONCodec[T]{}, + }, nil +} + +// Fetch fetches and parses a message to type T. +func (tc *TypedConsumer[T]) Fetch(ctx context.Context) (*messaging.TypedMessage[T], error) { + if tc == nil || tc.consumer == nil { + return nil, errors.New("kafka: typed consumer is nil") + } + if tc.consumer.reader == nil { + return nil, errors.New("kafka: consumer is nil") + } + + m, err := tc.consumer.reader.FetchMessage(ctx) + if err != nil { + return nil, err + } + + var value T + if err := tc.codec.Unmarshal(m.Value, &value); err != nil { + return nil, err + } + + return &messaging.TypedMessage[T]{Key: m.Key, Value: value, Raw: m.Value}, nil +} + +// Close closes the underlying consumer. +func (tc *TypedConsumer[T]) Close() error { + if tc == nil || tc.consumer == nil { + return nil + } + return tc.consumer.Close() +} diff --git a/rigour/internal/messaging/kafka/producer.go b/rigour/internal/messaging/kafka/producer.go new file mode 100644 index 0000000..8f8cf44 --- /dev/null +++ b/rigour/internal/messaging/kafka/producer.go @@ -0,0 +1,109 @@ +package kafka + +import ( + "context" + "errors" + "strings" + + "github.com/ctrlsam/rigour/internal/messaging" + + kafka "github.com/segmentio/kafka-go" +) + +type ProducerConfig struct { + Brokers string // Comma-separated list of broker addresses + Topic string +} + +type Producer struct { + writer *kafka.Writer +} + +func (c ProducerConfig) Validate() error { + if len(c.Brokers) == 0 { + return errors.New("kafka: brokers is empty") + } + if strings.TrimSpace(c.Topic) == "" { + return errors.New("kafka: topic is empty") + } + return nil +} + +func NewProducer(cfg ProducerConfig) (*Producer, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + + brokers := strings.Split(cfg.Brokers, ",") + for i := range brokers { + brokers[i] = strings.TrimSpace(brokers[i]) + } + + writer := &kafka.Writer{ + Addr: kafka.TCP(brokers...), + Topic: cfg.Topic, + Balancer: &kafka.LeastBytes{}, + AllowAutoTopicCreation: true, + BatchSize: 10, + BatchTimeout: 100, + Async: true, + } + + producer := &Producer{writer: writer} + + return producer, nil +} + +func (producer *Producer) Close() error { + if producer == nil || producer.writer == nil { + return nil + } + return producer.writer.Close() +} + +// TypedProducer[T] wraps a Producer and automatically marshals messages of type T. +type TypedProducer[T any] struct { + producer *Producer + codec messaging.Codec[T] +} + +var _ messaging.Producer[any] = (*TypedProducer[any])(nil) + +// NewTypedProducer creates a new producer that serializes messages of type T. +func NewTypedProducer[T any](cfg ProducerConfig) (*TypedProducer[T], error) { + producer, err := NewProducer(cfg) + if err != nil { + return nil, err + } + return &TypedProducer[T]{ + producer: producer, + codec: messaging.JSONCodec[T]{}, + }, nil +} + +// Publish publishes and serializes a message of type T. +func (tp *TypedProducer[T]) Publish(ctx context.Context, key []byte, value T) error { + if tp == nil || tp.producer == nil { + return errors.New("kafka: typed producer is nil") + } + + serialized, err := tp.codec.Marshal(value) + if err != nil { + return err + } + + if tp.producer.writer == nil { + return errors.New("kafka: producer is nil") + } + return tp.producer.writer.WriteMessages(ctx, + kafka.Message{Key: key, Value: serialized}, + ) +} + +// Close closes the underlying producer. +func (tp *TypedProducer[T]) Close() error { + if tp == nil || tp.producer == nil { + return nil + } + return tp.producer.Close() +} diff --git a/rigour/internal/messaging/messaging.go b/rigour/internal/messaging/messaging.go new file mode 100644 index 0000000..6c8c531 --- /dev/null +++ b/rigour/internal/messaging/messaging.go @@ -0,0 +1,20 @@ +package messaging + +import "context" + +// Producer publishes typed messages to a topic/stream. +// +// Implementations are expected to be safe for concurrent use. +// Key is optional but recommended for stable partitioning. +type Producer[T any] interface { + Publish(ctx context.Context, key []byte, value T) error + Close() error +} + +// Consumer subscribes to a topic/stream and yields typed messages. +// +// Implementations should honor ctx cancellation and return context.Canceled when appropriate. +type Consumer[T any] interface { + Fetch(ctx context.Context) (*TypedMessage[T], error) + Close() error +} diff --git a/rigour/internal/storage/mongodb/client.go b/rigour/internal/storage/mongodb/client.go new file mode 100644 index 0000000..a1bcd0a --- /dev/null +++ b/rigour/internal/storage/mongodb/client.go @@ -0,0 +1,88 @@ +package mongodb + +import ( + "context" + "fmt" + "time" + + "github.com/ctrlsam/rigour/internal/storage" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Client manages MongoDB connections and provides methods to create repositories. +type Client struct { + client *mongo.Client +} + +// NewClient creates a new MongoDB client and connects to the server. +func NewClient(ctx context.Context, uri string, timeout time.Duration) (*Client, error) { + if timeout <= 0 { + timeout = 10 * time.Second + } + + // Connect to MongoDB + cfg := Config{URI: uri, Database: "", Timeout: timeout} + client, _, err := Connect(ctx, cfg) + if err != nil { + return nil, fmt.Errorf("mongodb client: failed to connect: %w", err) + } + + return &Client{client: client}, nil +} + +// Config contains connection settings for MongoDB. +type Config struct { + URI string + Database string + Timeout time.Duration +} + +// Connect creates and verifies a MongoDB client connection. It returns the +// connected client and the database (may be nil if Database is empty). +func Connect(ctx context.Context, cfg Config) (*mongo.Client, *mongo.Database, error) { + connectCtx, cancel := context.WithTimeout(ctx, cfg.Timeout) + defer cancel() + + clientOpts := options.Client().ApplyURI(cfg.URI) + client, err := mongo.Connect(connectCtx, clientOpts) + if err != nil { + return nil, nil, err + } + + // Ping to ensure connection is established + pingCtx, cancelPing := context.WithTimeout(ctx, 5*time.Second) + defer cancelPing() + if err := client.Ping(pingCtx, nil); err != nil { + // try to disconnect on failure + _ = client.Disconnect(context.Background()) + return nil, nil, err + } + + var db *mongo.Database + if cfg.Database != "" { + db = client.Database(cfg.Database) + } + return client, db, nil +} + +// NewHostsRepository creates a new MongoDB hosts repository with the provided configuration. +func (c *Client) NewHostsRepository(ctx context.Context, cfg storage.RepositoryConfig) (storage.HostRepository, error) { + if c.client == nil { + return nil, fmt.Errorf("mongodb client: client is not initialized") + } + + db := c.client.Database(cfg.Database) + coll := db.Collection(cfg.Collection) + + // Delegate to hosts.go to create repository with proper indexes + return NewHostsRepository(ctx, coll) +} + +// Close disconnects the MongoDB client. +func (c *Client) Close(ctx context.Context) error { + if c.client != nil { + return c.client.Disconnect(ctx) + } + return nil +} diff --git a/rigour/internal/storage/mongodb/hosts.go b/rigour/internal/storage/mongodb/hosts.go new file mode 100644 index 0000000..d13befd --- /dev/null +++ b/rigour/internal/storage/mongodb/hosts.go @@ -0,0 +1,357 @@ +package mongodb + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/ctrlsam/rigour/internal/storage" + "github.com/ctrlsam/rigour/pkg/types" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type HostRepository struct { + collection *mongo.Collection +} + +func NewHostsRepository(ctx context.Context, coll *mongo.Collection) (storage.HostRepository, error) { + _, _ = coll.Indexes().CreateMany(ctx, []mongo.IndexModel{ + { + Keys: bson.D{{Key: "ip", Value: 1}}, + Options: options.Index().SetUnique(true).SetName("ip_unique"), + }, + { + Keys: bson.D{ + {Key: "ip", Value: 1}, + {Key: "services.port", Value: 1}, + {Key: "services.protocol", Value: 1}, + {Key: "services.transport", Value: 1}, + }, + Options: options.Index().SetName("services_lookup"), + }, + }) + return &HostRepository{collection: coll}, nil +} + +func (repo *HostRepository) EnsureHost(ctx context.Context, ip string, now time.Time) error { + if now.IsZero() { + now = time.Now() + } + + filter := bson.M{"ip": ip} + update := bson.M{ + "$setOnInsert": bson.M{ + "ip": ip, + "first_seen": now, + "services": []types.Service{}, + "labels": []string{}, + }, + "$set": bson.M{ + "last_seen": now, + }, + } + + _, err := repo.collection.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true)) + return err +} + +func (repo *HostRepository) UpdateHost(ctx context.Context, host types.Host) error { + now := time.Now() + set := bson.M{ + "last_seen": now, + } + + if host.ASN != nil { + set["asn"] = host.ASN + } + if host.Location != nil { + set["location"] = host.Location + } + if host.Labels != nil { + set["labels"] = host.Labels + } + if host.IPInt != 0 { + set["ip_int"] = host.IPInt + } + + _, err := repo.collection.UpdateOne(ctx, bson.M{"ip": host.IP}, bson.M{"$set": set}) + return err +} + +func (repo *HostRepository) GetByIP(ctx context.Context, ip string) (*types.Host, error) { + if repo == nil || repo.collection == nil { + return nil, errors.New("mongodb: hosts repository is nil") + } + + filter := bson.M{"ip": ip} + var host types.Host + err := repo.collection.FindOne(ctx, filter).Decode(&host) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, fmt.Errorf("host not found: %s", ip) + } + return nil, fmt.Errorf("mongodb: failed to fetch host: %w", err) + } + + return &host, nil +} + +func (repo *HostRepository) UpsertService(ctx context.Context, svc types.Service) error { + now := svc.LastScan + if now.IsZero() { + now = time.Now() + } + + // Replace existing service or push if missing + filter := bson.M{ + "ip": svc.IP, + "services": bson.M{ + "$elemMatch": bson.M{ + "port": svc.Port, + }, + }, + } + updateExisting := bson.M{ + "$set": bson.M{ + "services.$": svc, + "last_seen": now, + }, + } + + res, err := repo.collection.UpdateOne(ctx, filter, updateExisting) + if err != nil { + return err + } + if res.MatchedCount > 0 { + return nil + } + + // Not found, push it. + filterHost := bson.M{"ip": svc.IP} + pushUpdate := bson.M{ + "$set": bson.M{ + "last_seen": now, + }, + "$push": bson.M{"services": svc}, + } + _, err = repo.collection.UpdateOne(ctx, filterHost, pushUpdate) + if err != nil { + return fmt.Errorf("mongodb: push service: %w", err) + } + return nil +} + +func (repo *HostRepository) Search(ctx context.Context, filter map[string]interface{}, lastID string, limit int) ([]types.Host, string, error) { + if repo == nil || repo.collection == nil { + return nil, "", errors.New("mongodb: hosts repository is nil") + } + + // Build the match stage from the filter + matchStage := bson.M{} + if len(filter) > 0 { + matchStage = bson.M(filter) + } + + // Build the pipeline + pipeline := mongo.Pipeline{ + bson.D{{Key: "$match", Value: matchStage}}, + } + + // If lastID is provided, add a filter to skip past it + if lastID != "" { + pipeline = append(pipeline, bson.D{ + {Key: "$match", Value: bson.M{"_id": bson.M{"$gt": lastID}}}, + }) + } + + // Sort by _id and limit to get one extra to check if there are more results + pipeline = append(pipeline, + bson.D{{Key: "$sort", Value: bson.M{"_id": 1}}}, + bson.D{{Key: "$limit", Value: limit + 1}}, + ) + + cursor, err := repo.collection.Aggregate(ctx, pipeline) + if err != nil { + return nil, "", fmt.Errorf("mongodb: search aggregation failed: %w", err) + } + defer cursor.Close(ctx) + + var hosts []types.Host + if err := cursor.All(ctx, &hosts); err != nil { + return nil, "", fmt.Errorf("mongodb: failed to decode results: %w", err) + } + + // Determine if there are more results + var nextID string + if len(hosts) > limit { + // There are more results, trim to limit and set next ID + hosts = hosts[:limit] + nextID = hosts[len(hosts)-1].ID + } + + return hosts, nextID, nil +} + +func (repo *HostRepository) Facets(ctx context.Context, filter map[string]interface{}) (*storage.FacetCounts, error) { + if repo == nil || repo.collection == nil { + return nil, errors.New("mongodb: hosts repository is nil") + } + + // Build the match stage from the filter + matchStage := bson.M{} + if len(filter) > 0 { + matchStage = bson.M(filter) + } + + // Build the aggregation pipeline with facets + pipeline := mongo.Pipeline{ + bson.D{{Key: "$match", Value: matchStage}}, + bson.D{ + {Key: "$facet", Value: bson.M{ + "services": bson.A{ + bson.M{"$unwind": "$services"}, + bson.M{"$group": bson.M{ + "_id": "$services.protocol", + "count": bson.M{"$sum": 1}, + }}, + bson.M{"$sort": bson.M{"count": -1}}, + }, + "countries": bson.A{ + bson.M{"$match": bson.M{"location.country_code": bson.M{"$exists": true, "$ne": nil}}}, + bson.M{"$group": bson.M{ + "_id": bson.M{ + "code": "$location.country_code", + "name": "$location.country_name", + }, + "count": bson.M{"$sum": 1}, + }}, + bson.M{"$sort": bson.M{"count": -1}}, + }, + "asns": bson.A{ + bson.M{"$match": bson.M{"asn.number": bson.M{"$exists": true, "$ne": nil}}}, + bson.M{"$group": bson.M{ + "_id": bson.M{ + "number": "$asn.number", + "organization": "$asn.organization", + }, + "count": bson.M{"$sum": 1}, + }}, + bson.M{"$sort": bson.M{"count": -1}}, + }, + }}, + }, + } + + cursor, err := repo.collection.Aggregate(ctx, pipeline) + if err != nil { + return nil, fmt.Errorf("mongodb: facet aggregation failed: %w", err) + } + defer cursor.Close(ctx) + + var results []bson.M + if err := cursor.All(ctx, &results); err != nil { + return nil, fmt.Errorf("mongodb: failed to decode facet results: %w", err) + } + + if len(results) == 0 { + return &storage.FacetCounts{ + Services: make(map[string]int), + Countries: []storage.CountryFacet{}, + ASNs: []storage.ASNFacet{}, + }, nil + } + + facetResult := results[0] + counts := &storage.FacetCounts{ + Services: make(map[string]int), + Countries: []storage.CountryFacet{}, + ASNs: []storage.ASNFacet{}, + } + + // Process services facet + if servicesFacet, ok := facetResult["services"]; ok { + if servicesArray, ok := servicesFacet.(bson.A); ok { + for _, item := range servicesArray { + if doc, ok := item.(bson.M); ok { + if id, ok := doc["_id"]; ok { + if count, ok := doc["count"].(int32); ok { + counts.Services[fmt.Sprintf("%v", id)] = int(count) + } + } + } + } + } + } + + // Process countries facet + if countriesFacet, ok := facetResult["countries"]; ok { + if countriesArray, ok := countriesFacet.(bson.A); ok { + for _, item := range countriesArray { + if doc, ok := item.(bson.M); ok { + if id, ok := doc["_id"]; ok { + if count, ok := doc["count"].(int32); ok { + // Extract code and name from the grouped _id + if idMap, ok := id.(bson.M); ok { + code := fmt.Sprintf("%v", idMap["code"]) + name := fmt.Sprintf("%v", idMap["name"]) + counts.Countries = append(counts.Countries, storage.CountryFacet{ + Code: code, + Name: name, + Count: int(count), + }) + } + } + } + } + } + } + } + + // Process ASNs facet + if asnsFacet, ok := facetResult["asns"]; ok { + if asnsArray, ok := asnsFacet.(bson.A); ok { + for _, item := range asnsArray { + if doc, ok := item.(bson.M); ok { + if id, ok := doc["_id"]; ok { + if count, ok := doc["count"].(int32); ok { + if idMap, ok := id.(bson.M); ok { + var asnCode uint32 + if numberVal, ok := idMap["number"]; ok { + // Handle both int32 and uint32 types + switch v := numberVal.(type) { + case int32: + asnCode = uint32(v) + case uint32: + asnCode = v + case int64: + asnCode = uint32(v) + case float64: + asnCode = uint32(v) + } + } + + var organization string + if orgVal, ok := idMap["organization"]; ok { + organization = fmt.Sprintf("%v", orgVal) + } + + counts.ASNs = append(counts.ASNs, storage.ASNFacet{ + Code: asnCode, + Name: organization, + Count: int(count), + }) + } + } + } + } + } + } + } + + return counts, nil +} + +var _ storage.HostRepository = (*HostRepository)(nil) diff --git a/rigour/internal/storage/repositories.go b/rigour/internal/storage/repositories.go new file mode 100644 index 0000000..9410aa8 --- /dev/null +++ b/rigour/internal/storage/repositories.go @@ -0,0 +1,63 @@ +package storage + +import ( + "context" + "time" + + "github.com/ctrlsam/rigour/pkg/types" +) + +// RepositoryConfig holds configuration for repository initialization. +type RepositoryConfig struct { + URI string + Database string + Collection string + Timeout int // in seconds +} + +// CountryFacet represents a country facet with code and name. +type CountryFacet struct { + Code string `json:"code"` + Name string `json:"name"` + Count int `json:"count"` +} + +// ASNFacet represents an ASN facet with code, name, and count. +type ASNFacet struct { + Code uint32 `json:"code"` + Name string `json:"name"` + Count int `json:"count"` +} + +// FacetCounts represents aggregated counts for various facets. +type FacetCounts struct { + Services map[string]int `json:"services,omitempty"` + Countries []CountryFacet `json:"countries,omitempty"` + ASNs []ASNFacet `json:"asns,omitempty"` +} + +// HostRepository is the interface for storing and querying host records. +type HostRepository interface { + // EnsureHost ensures a host record exists for ip. + // Implementations should be idempotent. + EnsureHost(ctx context.Context, ip string, now time.Time) error + + // UpsertService stores/updates a single service under its host. + // Implementations should create the host if it doesn't exist. + UpsertService(ctx context.Context, svc types.Service) error + + // UpdateHost updates top-level host fields (ASN/Location/Labels/etc). + // Implementations may upsert. + UpdateHost(ctx context.Context, host types.Host) error + + // GetByIP retrieves a single host by IP address. + // Returns the host or an error if not found. + GetByIP(ctx context.Context, ip string) (*types.Host, error) + + // Search queries hosts with filter and pagination support. + // Returns hosts, next cursor ID, and error. + Search(ctx context.Context, filter map[string]interface{}, lastID string, limit int) ([]types.Host, string, error) + + // Facets performs aggregation for facet counts. + Facets(ctx context.Context, filter map[string]interface{}) (*FacetCounts, error) +} diff --git a/rigour/pkg/api/models.go b/rigour/pkg/api/models.go new file mode 100644 index 0000000..54340d9 --- /dev/null +++ b/rigour/pkg/api/models.go @@ -0,0 +1,47 @@ +package api + +import ( + "github.com/ctrlsam/rigour/internal/storage" + "github.com/ctrlsam/rigour/pkg/types" +) + +// Alias storage facet types for API usage +type CountryFacet = storage.CountryFacet +type ASNFacet = storage.ASNFacet + +// SearchRequest represents a search query request. +type SearchRequest struct { + Filter map[string]interface{} `json:"filter,omitempty"` + PageToken string `json:"page_token,omitempty"` + Limit int `json:"limit,omitempty"` +} + +// SearchResponse represents the response for a search query. +type SearchResponse struct { + Hosts []types.Host `json:"hosts"` + Facets *FacetCounts `json:"facets,omitempty"` + NextPageToken string `json:"next_page_token,omitempty"` +} + +// FacetCounts represents aggregated counts for various facets. +type FacetCounts struct { + Services map[string]int `json:"services,omitempty"` + Countries []CountryFacet `json:"countries,omitempty"` + ASNs []ASNFacet `json:"asns,omitempty"` +} + +// FacetRequest represents a request for facet aggregation. +type FacetRequest struct { + Filter map[string]interface{} `json:"filter,omitempty"` +} + +// FacetResponse represents the response for a facet aggregation query. +type FacetResponse struct { + Facets FacetCounts `json:"facets"` +} + +// PaginationToken represents the pagination cursor token structure. +type PaginationToken struct { + LastID string `json:"last_id"` + SortField string `json:"sort_field"` +} diff --git a/rigour/pkg/api/pagination.go b/rigour/pkg/api/pagination.go new file mode 100644 index 0000000..750a8a9 --- /dev/null +++ b/rigour/pkg/api/pagination.go @@ -0,0 +1,58 @@ +package api + +import ( + "encoding/base64" + "encoding/json" + "fmt" +) + +const ( + DefaultPageSize = 50 + MaxPageSize = 500 +) + +// EncodePaginationToken encodes a pagination token to a base64 string. +func EncodePaginationToken(lastID string, sortField string) (string, error) { + token := PaginationToken{ + LastID: lastID, + SortField: sortField, + } + + data, err := json.Marshal(token) + if err != nil { + return "", fmt.Errorf("failed to marshal pagination token: %w", err) + } + + encoded := base64.StdEncoding.EncodeToString(data) + return encoded, nil +} + +// DecodePaginationToken decodes a base64 pagination token. +func DecodePaginationToken(token string) (*PaginationToken, error) { + if token == "" { + return nil, nil + } + + data, err := base64.StdEncoding.DecodeString(token) + if err != nil { + return nil, fmt.Errorf("failed to decode pagination token: %w", err) + } + + var pt PaginationToken + if err := json.Unmarshal(data, &pt); err != nil { + return nil, fmt.Errorf("failed to unmarshal pagination token: %w", err) + } + + return &pt, nil +} + +// ValidatePageSize ensures the page size is within acceptable bounds. +func ValidatePageSize(size int) int { + if size <= 0 { + return DefaultPageSize + } + if size > MaxPageSize { + return MaxPageSize + } + return size +} diff --git a/rigour/pkg/crawler/discovery/naabu/naabu.go b/rigour/pkg/crawler/discovery/naabu/naabu.go new file mode 100644 index 0000000..6339209 --- /dev/null +++ b/rigour/pkg/crawler/discovery/naabu/naabu.go @@ -0,0 +1,65 @@ +package naabu + +import ( + "context" + "fmt" + "strings" + + "github.com/ctrlsam/rigour/pkg/crawler/discovery" + "github.com/projectdiscovery/goflags" + naabuResult "github.com/projectdiscovery/naabu/v2/pkg/result" + naabuRunner "github.com/projectdiscovery/naabu/v2/pkg/runner" +) + +// Run executes Naabu discovery for a single input target and invokes onResult +// for each open port found. +func Run(ctx context.Context, ipRange string, opts discovery.DiscoveryConfig, onResult func(discovery.Result)) error { + if strings.TrimSpace(ipRange) == "" { + return fmt.Errorf("naabu discovery input is empty") + } + + naabuOpts := &naabuRunner.Options{ + Host: goflags.StringSlice{ipRange}, + // caller-configurable + ScanType: opts.ScanType, + Ports: opts.Ports, + TopPorts: opts.TopPorts, + Rate: opts.Rate, + Retries: opts.Retries, + Threads: 100, + //Silent: true, + } + + naabuOpts.OnReceive = func(hr *naabuResult.HostResult) { + for _, p := range hr.Ports { + //fmt.Println("[DISCOVERY] Open port found:", hr.IP, p.Port) + onResult(discovery.Result{ + Host: hr.IP, + Port: p.Port, + Protocol: p.Protocol.String(), + }) + } + } + + r, err := naabuRunner.NewRunner(naabuOpts) + if err != nil { + return fmt.Errorf("naabu.NewRunner failed: %w", err) + } + defer r.Close() + + // Naabu runner is not fully context-aware; honour ctx by stopping early if canceled. + done := make(chan error, 1) + go func() { + done <- r.RunEnumeration(ctx) + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-done: + if err != nil { + return fmt.Errorf("naabu enumeration failed: %w", err) + } + return nil + } +} diff --git a/rigour/pkg/crawler/discovery/types.go b/rigour/pkg/crawler/discovery/types.go new file mode 100644 index 0000000..43b4a75 --- /dev/null +++ b/rigour/pkg/crawler/discovery/types.go @@ -0,0 +1,15 @@ +package discovery + +type DiscoveryConfig struct { + ScanType string + Ports string + TopPorts string + Retries int + Rate int +} + +type Result struct { + Host string + Port int + Protocol string +} diff --git a/rigour/pkg/crawler/fingerprint/config.go b/rigour/pkg/crawler/fingerprint/config.go new file mode 100644 index 0000000..76c64f7 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/config.go @@ -0,0 +1,19 @@ +package fingerprint + +import ( + "time" +) + +type FingerprintConfig struct { + // UDP scan + UDP bool + + FastMode bool + + // The timeout specifies how long certain tasks should wait during the scanning process. + // This may include the timeouts set on the handshake process and the time to wait for a response to return. + DefaultTimeout time.Duration + + // Prints logging messages to stderr + Verbose bool +} diff --git a/rigour/pkg/crawler/fingerprint/plugin_list.go b/rigour/pkg/crawler/fingerprint/plugin_list.go new file mode 100644 index 0000000..b8865f3 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugin_list.go @@ -0,0 +1,43 @@ +package fingerprint + +// These import statements ensure that the init functions run in each plugin. +// When a new plugin is added, this list should be updated. + +import ( + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/dhcp" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/dns" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/echo" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/ftp" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/http" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/imap" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/ipmi" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/ipsec" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/jdwp" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaNew" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaOld" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/ldap" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/linuxrpc" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/minecraft/java" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/modbus" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt3" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt5" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/mssql" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/mysql" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/netbios" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/ntp" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/openvpn" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/oracledb" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/pop3" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/postgresql" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/rdp" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/redis" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/rsync" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/rtsp" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/smb" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/smtp" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/snmp" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/ssh" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/stun" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/telnet" + _ "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/services/vnc" +) diff --git a/rigour/pkg/crawler/fingerprint/plugins/plugins.go b/rigour/pkg/crawler/fingerprint/plugins/plugins.go new file mode 100644 index 0000000..9c7a221 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/plugins.go @@ -0,0 +1,54 @@ +package plugins + +import "fmt" + +var Plugins = make(map[Protocol][]Plugin) +var pluginIDs = make(map[PluginID]bool) + +// This function must not be run concurrently. +// This function should only be run once per plugin. +func RegisterPlugin(p Plugin) { + id := CreatePluginID(p) + if pluginIDs[id] { + panic(fmt.Sprintf("plugin: Register called twice for driver %+v\n", id)) + } + + pluginIDs[id] = true + + var pluginList []Plugin + if list, exists := Plugins[p.Type()]; exists { + pluginList = list + } else { + pluginList = make([]Plugin, 0) + } + + Plugins[p.Type()] = append(pluginList, p) +} + +func (p Protocol) String() (s string) { + switch p { + case IP: + s = "IP" + case TCP: + s = "TCP" + case TCPTLS: + s = "TCPTLS" + case UDP: + s = "UDP" + default: + panic("No string name for protocol %d.") + } + + return +} + +func CreatePluginID(p Plugin) PluginID { + return PluginID{ + name: p.Name(), + protocol: p.Type(), + } +} + +func (p PluginID) String() string { + return fmt.Sprintf("%s/%v", p.protocol, p.name) +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/pluginutils/error.go b/rigour/pkg/crawler/fingerprint/plugins/pluginutils/error.go new file mode 100644 index 0000000..de66f75 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/pluginutils/error.go @@ -0,0 +1,134 @@ +package pluginutils + +import "fmt" + +type RandomizeError struct { + Message string +} + +type InvalidResponseError struct { + Service string +} + +type InvalidResponseErrorInfo struct { + Service string + Info string +} + +type WriteTimeoutError struct { + WrappedError error +} + +type ReadTimeoutError struct { + WrappedError error +} + +type WriteError struct { + WrappedError error +} + +type ReadError struct { + Info string + WrappedError error +} + +type CreateDialError struct { + Message string +} + +type CloseDialError struct { +} + +type RequestError struct { + Message string +} + +type ServerNotEnable struct { +} + +type InvalidAddrProvided struct { + Service string +} + +func (e *RandomizeError) Error() string { + return fmt.Sprintf("failed to generate random bytes [%s]", e.Message) +} + +func (e *InvalidResponseError) Error() string { + return fmt.Sprintf("invalid %s response", e.Service) +} + +func (e *InvalidResponseErrorInfo) Error() string { + return fmt.Sprintf("invalid %s response, %s", e.Service, e.Info) +} + +func (e *WriteTimeoutError) Error() string { + errString := "failed to set timeout value for write" + if e.WrappedError != nil { + errString = fmt.Sprintf("%s (Error: %s)", errString, e.WrappedError.Error()) + } + return errString +} + +func (e *WriteTimeoutError) Unwrap() error { + return e.WrappedError +} + +func (e *ReadTimeoutError) Error() string { + errString := "failed to set timeout value for read" + if e.WrappedError != nil { + errString = fmt.Sprintf("%s (Error: %s)", errString, e.WrappedError.Error()) + } + return errString +} + +func (e *ReadTimeoutError) Unwrap() error { + return e.WrappedError +} + +func (e *WriteError) Error() string { + errString := "failed to send out packet" + if e.WrappedError != nil { + errString = fmt.Sprintf("%s (Error: %s)", errString, e.WrappedError.Error()) + } + return errString +} + +func (e *WriteError) Unwrap() error { + return e.WrappedError +} + +func (e *ReadError) Error() string { + errString := "failed to receive packet" + if len(e.Info) > 0 { + errString = fmt.Sprintf("%s (Info: %s)", errString, e.Info) + } + if e.WrappedError != nil { + errString = fmt.Sprintf("%s (Error: %s)", errString, e.WrappedError.Error()) + } + return errString +} + +func (e *ReadError) Unwrap() error { + return e.WrappedError +} + +func (e *CreateDialError) Error() string { + return fmt.Sprintf("failed to create connection: %s", e.Message) +} + +func (e *CloseDialError) Error() string { + return "failed to close connection" +} + +func (e *RequestError) Error() string { + return fmt.Sprintf("failed to send request, %s", e.Message) +} + +func (e *ServerNotEnable) Error() string { + return "server is not enabled" +} + +func (e *InvalidAddrProvided) Error() string { + return fmt.Sprintf("a valid address is required for %s service", e.Service) +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/pluginutils/requests.go b/rigour/pkg/crawler/fingerprint/plugins/pluginutils/requests.go new file mode 100644 index 0000000..d78079d --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/pluginutils/requests.go @@ -0,0 +1,60 @@ +package pluginutils + +import ( + "encoding/hex" + "errors" + "fmt" + "net" + "syscall" + "time" +) + +func Send(conn net.Conn, data []byte, timeout time.Duration) error { + err := conn.SetWriteDeadline(time.Now().Add(timeout)) + if err != nil { + return &WriteTimeoutError{WrappedError: err} + } + length, err := conn.Write(data) + if err != nil { + return &WriteError{WrappedError: err} + } + if length < len(data) { + return &WriteError{ + WrappedError: fmt.Errorf( + "Failed to write all bytes (%d bytes written, %d bytes expected)", + length, + len(data), + ), + } + } + return nil +} + +func Recv(conn net.Conn, timeout time.Duration) ([]byte, error) { + response := make([]byte, 4096) + err := conn.SetReadDeadline(time.Now().Add(timeout)) + if err != nil { + return []byte{}, &ReadTimeoutError{WrappedError: err} + } + length, err := conn.Read(response) + if err != nil { + var netErr net.Error + if (errors.As(err, &netErr) && netErr.Timeout()) || + errors.Is(err, syscall.ECONNREFUSED) { // timeout error or connection refused + return []byte{}, nil + } + return response[:length], &ReadError{ + Info: hex.EncodeToString(response[:length]), + WrappedError: err, + } + } + return response[:length], nil +} + +func SendRecv(conn net.Conn, data []byte, timeout time.Duration) ([]byte, error) { + err := Send(conn, data, timeout) + if err != nil { + return []byte{}, err + } + return Recv(conn, timeout) +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/dhcp/dhcp.go b/rigour/pkg/crawler/fingerprint/plugins/services/dhcp/dhcp.go new file mode 100644 index 0000000..230e900 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/dhcp/dhcp.go @@ -0,0 +1,368 @@ +package dhcp + +import ( + "bytes" + "crypto/rand" + "fmt" + "math/big" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const DHCP = "dhcp" + +type Plugin struct{} + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func getSignatures() map[int]string { + signature := map[int]string{ + 0: "Pad", + 1: "Subnet Mask", + 2: "Time Offset", + 3: "Router", + 4: "Time Server", + 5: "Name Server", + 6: "Domain Server", + 7: "Log Server", + 8: "Quotes Server", + 9: "LPR Server", + 10: "Impress Server", + 11: "RLP Server", + 12: "Hostname", + 13: "Boot File Size", + 14: "Merit Dump File", + 15: "Domain Name", + 16: "Swap Server", + 17: "Root Path", + 18: "Extension File", + 19: "Forward On/Off", + 20: "SrcRte On/Off", + 21: "Policy Filter", + 22: "Max DG Assembly", + 23: "Default IP TTL", + 24: "MTU Timeout", + 25: "MTU Plateau", + 26: "MTU Interface", + 27: "MTU Subnet", + 28: "Broadcast Address", + 29: "Mask Discovery", + 30: "Mask Supplier", + 31: "Router Discovery", + 32: "Router Request", + 33: "Static Route", + 34: "Trailers", + 35: "ARP Timeout", + 36: "Ethernet", + 37: "Default TCP TTL", + 38: "Keepalive Time", + 39: "Keepalive Data", + 40: "NIS Domain", + 41: "NIS Servers", + 42: "NTP Servers", + 43: "Vendor Specific", + 44: "NETBIOS Name Srv", + 45: "NETBIOS Dist Srv", + 46: "NETBIOS Node Type", + 47: "NETBIOS Scope", + 48: "X Window Font", + 49: "X Window Manager", + 50: "Address Request", + 51: "Address Time", + 52: "Overload", + 53: "DHCP Msg Type", + 54: "DHCP Server Id", + 55: "Parameter List", + 56: "DHCP Message", + 57: "DHCP Max Msg Size", + 58: "Renewal Time", + 59: "Rebinding Time", + 60: "Class Id", + 61: "Client Id", + 62: "NetWare/IP Domain", + 63: "NetWare/IP Option", + 64: "NIS-Domain-Name", + 65: "NIS-Server-Addr", + 66: "Server-Name", + 67: "Bootfile-Name", + 68: "Home-Agent-Addrs", + 69: "SMTP-Server", + 70: "POP3-Server", + 71: "NNTP-Server", + 72: "WWW-Server", + 73: "Finger-Server", + 74: "IRC-Server", + 75: "StreetTalk-Server", + 76: "STDA-Server", + 77: "User-Class", + 78: "Directory Agent", + 79: "Service Scope", + 80: "Rapid Commit", + 81: "Client FQDN", + 82: "Relay Agent Information", + 83: "iSNS", + 85: "NDS Servers", + 86: "NDS Tree Name", + 87: "NDS Context", + 88: "BCMCS Controller Domain Name list", + 89: "BCMCS Controller IPv4 address option", + 90: "Authentication", + 91: "client-last-transaction-time option", + 92: "associated-ip option", + 93: "Client System", + 94: "Client NDI", + 95: "LDAP", + 97: "UUID/GUID", + 98: "User-Auth", + 99: "GEOCONF_CIVIC", + 100: "PCode", + 101: "TCode", + 109: "OPTION_DHCP4O6_S46_SADDR", + 112: "Netinfo Address", + 113: "Netinfo Tag", + 114: "URL", + 116: "Auto-Config", + 117: "Name Service Search", + 118: "Subnet Selection Option", + 119: "Domain Search", + 120: "SIP Servers DHCP Option", + 121: "Classless", + 122: "CCC", + 123: "GeoConf Option", + 124: "V-I Vendor", + 125: "V-I Vendor-Specific Information", + 131: "Remote statistics server IP address", + 132: "IEEE 802.1Q VLAN ID", + 133: "IEEE 802.1D/p Layer", + 134: "Diffserv Code Point", + 135: "HTTP Proxy for phone-specific applications", + 136: "OPTION_PANA_AGENT", + 137: "OPTION_V4_LOST", + 138: "OPTION_CAPWAP_AC_V4", + 139: "OPTION-IPv4_Address-MoS", + 140: "OPTION-IPv4_FQDN-MoS", + 141: "SIP UA Configuration Service Domains", + 142: "OPTION-IPv4_Address-ANDSF", + 143: "OPTION_V4_SZTP_REDIRECT", + 144: "GeoLoc", + 145: "FORCERENEW_NONCE_CAPABLE", + 146: "RDNSS Selection", + 151: "status-code", + 152: "base-time", + 153: "start-time-of-state", + 154: "query-start-time", + 155: "query-end-time", + 156: "dhcp-state", + 157: "data-source", + 158: "OPTION_V4_PCP_SERVER", + 160: "DHCP Captive-Portal", + 161: "OPTION_MUD_URL_V4", + 175: "Etherboot", + 176: "IP Telephone", + 209: "Configuration File", + 210: "Path Prefix", + 211: "Reboot Time", + 212: "OPTION_6RD", + 213: "OPTION_V4_ACCESS_DOMAIN", + 220: "Subnet Allocation Option", + 221: "Virtual Subnet Selection", + 255: "End", + } + return signature +} + +func hostnameParse(options []byte) []string { + var ret string + var retList []string + wholePacket := options[2 : 2+int(options[1])] + packet := wholePacket + for len(packet) != 0 { + length := int(packet[0]) + if len(packet) < length+1 { + return retList + } + if length == 0 { + retList = append(retList, ret) + ret = "" + packet = packet[1:] + } else { + ret += string(packet[1 : 1+length]) + packet = packet[1+length:] + if len(packet) == 0 { + break + } + if packet[0] != 0 { + ret += "." + } + if packet[0] == 0xc0 && len(packet) == 2 { + wholePacket = wholePacket[int(packet[1]) : len(wholePacket)-(4+length)] + packet = wholePacket + } + } + } + retList = append(retList, ret) + return retList +} + +func ipParse(options []byte) []string { + ipLen := int(options[1]) / 4 + ipList := options[2 : 2+int(options[1])] + var ipStrList []string + for ipLen != 0 { + ip := fmt.Sprintf("%d.%d.%d.%d", int(ipList[0]), int(ipList[1]), int(ipList[2]), int(ipList[3])) + ipStrList = append(ipStrList, ip) + ipLen-- + ipList = ipList[4:] + } + return ipStrList +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + sliceIP := net.ParseIP("127.0.0.1") + if sliceIP == nil { + return nil, &utils.InvalidAddrProvided{Service: DHCP} + } + + LocalIP := []byte{sliceIP[12], sliceIP[13], sliceIP[14], sliceIP[15]} + InitialConnectionPackage := []byte{ + 0x01, // Message type: Boot Request (1) + 0x01, // Hardware type: Ethernet (0x01) + 0x06, // Hardware address length: 6 + 0x01, // Hops: 1 + } + transactionID := make([]byte, 4) + _, err := rand.Read(transactionID) + if err != nil { + return nil, &utils.RandomizeError{Message: "Transaction ID"} + } + InitialConnectionPackage = append(InitialConnectionPackage, transactionID...) + SecondPartConnectionPackage := []byte{ + 0x00, 0x00, // Seconds elapsed: 0 + 0x00, 0x00, // Bootp flags: 0x0000 (Unicast) + } + InitialConnectionPackage = append(InitialConnectionPackage, SecondPartConnectionPackage...) + IPConnectionPackage := []byte{ + 0x00, 0x00, 0x00, 0x00, // Client IP address: 0.0.0.0 + 0x00, 0x00, 0x00, 0x00, // Your (client) IP address: 0.0.0.0 + 0x00, 0x00, 0x00, 0x00, // Next server IP address: 0.0.0.0 + } + InitialConnectionPackage = append(InitialConnectionPackage, IPConnectionPackage...) + InitialConnectionPackage = append(InitialConnectionPackage, LocalIP...) // Relay server IP address: LocalIP + ClientMAC := make([]byte, 6) + _, err = rand.Read(ClientMAC) + if err != nil { + return nil, &utils.RandomizeError{Message: "ClientMAC"} + } + InitialConnectionPackage = append(InitialConnectionPackage, ClientMAC...) + ThirdPartConnectionPackage := []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client hardware address padding + // Server host name + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Boot File name + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x63, 0x82, 0x53, 0x63, // Magic cookie: DHCP + + 0x35, 0x01, 0x01, // Option: (53) DHCP Message Type (Discover) + } + InitialConnectionPackage = append(InitialConnectionPackage, ThirdPartConnectionPackage...) + InitialConnectionPackage = append(InitialConnectionPackage, 0xff) // Option: (255) End + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) < 8 { + return nil, nil + } + // https://ecanet.ir/dhcp-option-list/ + if bytes.Equal(transactionID, response[4:8]) { + if len(response) <= 240 && response[len(response)-1] != 255 { + return nil, nil + } + + signature := getSignatures() + options := response[240:] + + optionList := map[string]any{} + for int(options[0]) != 255 { + if len(options) < int(options[1])+2 { + // packet corruption + payload := plugins.ServiceDHCP{ + Option: fmt.Sprintf("%s", optionList), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil + } + c := int(options[0]) + switch c { + case 51, 58, 59: + optionList[signature[c]] = big.NewInt(0).SetBytes(options[2 : 2+int(options[1])]).Uint64() + case 119: + outList := hostnameParse(options) + optionList[signature[c]] = outList + case 15: + optionList[signature[c]] = string(options[2 : 2+int(options[1])]) + case 1: + ipStrList := ipParse(options) + optionList[signature[c]] = ipStrList + if len(ipStrList) == 1 { + optionList[signature[c]] = ipStrList[0] + } + case 3, 6, 28, 42, 44, 54: + ipStrList := ipParse(options) + optionList[signature[c]] = ipStrList + default: + if int(options[1]) == 1 { + optionList[signature[c]] = int(options[2]) + } else { + optionList[signature[c]] = options[2 : 2+int(options[1])] + } + } + options = options[2+int(options[1]):] + } + payload := plugins.ServiceDHCP{ + Option: fmt.Sprintf("%s", optionList), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil + } + return nil, nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 67 +} + +func (p *Plugin) Name() string { + return DHCP +} + +func (p *Plugin) PortReject(u uint16) bool { + return u != 67 +} + +func (p *Plugin) SrcPort() uint16 { + return 67 +} + +func (p *Plugin) Priority() int { + return 100 +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/dhcp/dhcp_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/dhcp/dhcp_test.go new file mode 100644 index 0000000..a91e00e --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/dhcp/dhcp_test.go @@ -0,0 +1,43 @@ +package dhcp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/test" +) + +func TestDHCP(t *testing.T) { + // cwd, err := os.Getwd() + // if err != nil { + // t.Fatalf("failed to get current directory") + // } + // TODO more work is required to get this test working locally + testcases := []test.Testcase{ + // { + // Description: "dhcp", + // Port: 67, + // Protocol: plugins.UDP, + // Expected: func(res *plugins.PluginResults) bool { + // return res != nil + // }, + // RunConfig: dockertest.RunOptions{ + // Repository: "wastrachan/dhcpd", + // Mounts: []string{fmt.Sprintf("%s/dhcpd.conf:/config/dhcpd.conf", cwd)}, + // ExposedPorts: []string{"67/udp"}, + // }, + // }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/dhcp/dhcpd.conf b/rigour/pkg/crawler/fingerprint/plugins/services/dhcp/dhcpd.conf new file mode 100644 index 0000000..f6aa36c --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/dhcp/dhcpd.conf @@ -0,0 +1,47 @@ +# dhcpd.conf +# +# Sample configuration file for ISC dhcpd +# + +option domain-name "example.org"; +option domain-name-servers ns1.example.org, ns2.example.org; +default-lease-time 600; +max-lease-time 7200; +#ddns-update-style none; +authoritative; +log-facility local7; + +# No service will be given on this subnet, but declaring it allows the dhcp +# server to listen on this network +subnet 0.0.0.0 netmask 0.0.0.0 { + range 172.17.0.0 172.17.0.255; +} + +# This is a very basic subnet declaration. +subnet 10.254.239.0 netmask 255.255.255.224 { + range 10.254.239.10 10.254.239.20; + option routers rtr-239-0-1.example.org, rtr-239-0-2.example.org; +} + +# A slightly different configuration for an internal subnet. +subnet 10.5.5.0 netmask 255.255.255.224 { + range 10.5.5.26 10.5.5.30; + option domain-name-servers ns1.internal.example.org; + option domain-name "internal.example.org"; + option routers 10.5.5.1; + option broadcast-address 10.5.5.31; + default-lease-time 600; + max-lease-time 7200; +} + +# Fixed IP addresses can also be specified for hosts. These addresses +# should not also be listed as being available for dynamic assignment. +# Hosts for which fixed IP addresses have been specified can boot using +# BOOTP or DHCP. Hosts for which no fixed address is specified can only +# be booted with DHCP, unless there is an address range on the subnet +# to which a BOOTP client is connected which has the dynamic-bootp flag +# set. +host fantasia { + hardware ethernet 08:00:07:26:c0:a5; + fixed-address fantasia.example.com; +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/dns/dns.go b/rigour/pkg/crawler/fingerprint/plugins/services/dns/dns.go new file mode 100644 index 0000000..ba989cc --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/dns/dns.go @@ -0,0 +1,131 @@ +package dns + +import ( + "bytes" + "crypto/rand" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const DNS = "dns" + +type UDPPlugin struct{} +type TCPPlugin struct{} + +func init() { + plugins.RegisterPlugin(&UDPPlugin{}) + plugins.RegisterPlugin(&TCPPlugin{}) +} + +func CheckDNS(conn net.Conn, timeout time.Duration) (bool, error) { + for attempts := 0; attempts < 3; attempts++ { + transactionID := make([]byte, 2) + _, err := rand.Read(transactionID) + if err != nil { + return false, &utils.RandomizeError{Message: "Transaction ID"} + } + + InitialConnectionPackage := append(transactionID, []byte{ //nolint:gocritic + // Transaction ID + 0x01, 0x00, // Flags: 0x0100 Standard query + 0x00, 0x01, // Questions: 1 + 0x00, 0x00, // Answer RRs: 0 + 0x00, 0x00, // Authority RRs: 0 + 0x00, 0x00, // Additional RRs: 0 + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x04, 0x62, 0x69, 0x6e, 0x64, 0x00, // Name: version.bind + 0x00, 0x10, // Type: TXT (Text strings) (16) + 0x00, 0x03, // Class: CH (0x0003) + }...) + + if conn.RemoteAddr().Network() == "tcp" { + InitialConnectionPackage = append([]byte{0x00, 0x1e}, InitialConnectionPackage...) + } + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return false, err + } + + if len(response) == 0 { + return false, nil + } + + if conn.RemoteAddr().Network() == "udp" { + if !bytes.Equal(transactionID[0:1], response[0:1]) { + return false, nil + } + } + + if conn.RemoteAddr().Network() == "tcp" { + if !bytes.Equal(transactionID[0:1], response[2:3]) { + return false, nil + } + } + } + + return true, nil +} + +func (p *UDPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + isDNS, err := CheckDNS(conn, timeout) + if err != nil { + return nil, err + } + + if isDNS { + payload := plugins.ServiceDNS{} + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil + } + + return nil, nil +} + +func (p *UDPPlugin) PortPriority(i uint16) bool { + return i == 53 +} + +func (p UDPPlugin) Name() string { + return DNS +} + +func (p *UDPPlugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p TCPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + isDNS, err := CheckDNS(conn, timeout) + if err != nil { + return nil, err + } + + if isDNS { + payload := plugins.ServiceDNS{} + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + + return nil, nil +} + +func (p TCPPlugin) PortPriority(i uint16) bool { + return i == 53 +} + +func (p TCPPlugin) Name() string { + return DNS +} + +func (p *TCPPlugin) Priority() int { + return 50 +} + +func (p *UDPPlugin) Priority() int { + return 50 +} + +func (p TCPPlugin) Type() plugins.Protocol { + return plugins.TCP +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/dns/dns_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/dns/dns_test.go new file mode 100644 index 0000000..6b443af --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/dns/dns_test.go @@ -0,0 +1,40 @@ +package dns + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestDNS(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "dns", + Port: 53, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "ruudud/devdns", + Mounts: []string{"/var/run/docker.sock:/var/run/docker.sock:ro"}, + Privileged: true, + }, + }, + } + + var p *UDPPlugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/echo/echo.go b/rigour/pkg/crawler/fingerprint/plugins/services/echo/echo.go new file mode 100644 index 0000000..3e46f5b --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/echo/echo.go @@ -0,0 +1,62 @@ +package echo + +import ( + "bytes" + "crypto/rand" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type EchoPlugin struct{} + +const ECHO = "echo" + +func isEcho(conn net.Conn, timeout time.Duration) (bool, error) { + // Generate a random 64 byte payload + payload := make([]byte, 64) + if _, err := rand.Read(payload); err != nil { + return false, err + } + + response, err := pluginutils.SendRecv(conn, payload, timeout) + if err != nil { + return false, err + } + + // Check if the response matches the payload + isEchoService := bytes.Equal(payload, response) + + return isEchoService, nil +} + +func init() { + plugins.RegisterPlugin(&EchoPlugin{}) +} + +func (p *EchoPlugin) PortPriority(port uint16) bool { + return port == 7 +} + +func (p *EchoPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + if isEcho, err := isEcho(conn, timeout); !isEcho || err != nil { + return nil, nil + } + payload := plugins.ServiceEcho{} + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *EchoPlugin) Name() string { + return ECHO +} + +func (p *EchoPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *EchoPlugin) Priority() int { + return 1 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/echo/echo_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/echo/echo_test.go new file mode 100644 index 0000000..a6cfd84 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/echo/echo_test.go @@ -0,0 +1,41 @@ +package echo + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestEcho(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "echo", + Port: 7, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "itsthenetwork/alpine-ncat", + Cmd: []string{"-e", "/bin/cat", "-k", "-l", "-p", "7"}, + Entrypoint: []string{"/usr/bin/ncat"}, + ExposedPorts: []string{"7"}, + }, + }, + } + + p := &EchoPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ftp/ftp.go b/rigour/pkg/crawler/fingerprint/plugins/services/ftp/ftp.go new file mode 100644 index 0000000..25c3dcf --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ftp/ftp.go @@ -0,0 +1,57 @@ +package ftp + +import ( + "net" + "regexp" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +var ftpResponse = regexp.MustCompile(`^\d{3}[- ](.*)\r`) + +const FTP = "ftp" + +type FTPPlugin struct{} + +func init() { + plugins.RegisterPlugin(&FTPPlugin{}) +} + +func (p *FTPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + response, err := utils.Recv(conn, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + matches := ftpResponse.FindStringSubmatch(string(response)) + if matches == nil { + return nil, nil + } + + payload := plugins.ServiceFTP{ + Banner: string(response), + } + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *FTPPlugin) PortPriority(i uint16) bool { + return i == 21 +} + +func (p *FTPPlugin) Name() string { + return FTP +} + +func (p *FTPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *FTPPlugin) Priority() int { + return 10 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ftp/ftp_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/ftp/ftp_test.go new file mode 100644 index 0000000..3f5cd02 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ftp/ftp_test.go @@ -0,0 +1,38 @@ +package ftp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestFTP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ftp", + Port: 21, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "panubo/vsftpd", + }, + }, + } + + p := &FTPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/http/http.go b/rigour/pkg/crawler/fingerprint/plugins/services/http/http.go new file mode 100644 index 0000000..4a68bef --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/http/http.go @@ -0,0 +1,224 @@ +package http + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "syscall" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" + wappalyzer "github.com/projectdiscovery/wappalyzergo" +) + +type HTTPPlugin struct { + analyzer *wappalyzer.Wappalyze +} +type HTTPSPlugin struct { + analyzer *wappalyzer.Wappalyze +} + +const HTTP = "http" +const HTTPS = "https" +const USERAGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" + +func init() { + wappalyzerClient, err := wappalyzer.New() + if err != nil { + panic("unable to initialize wappalyzer library") + } + plugins.RegisterPlugin(&HTTPPlugin{analyzer: wappalyzerClient}) + plugins.RegisterPlugin(&HTTPSPlugin{analyzer: wappalyzerClient}) +} + +var ( + commonHTTPPorts = map[int]struct{}{ + 80: {}, + 3000: {}, + 4567: {}, + 5000: {}, + 8000: {}, + 8001: {}, + 8080: {}, + 8081: {}, + 8888: {}, + 9001: {}, + 9080: {}, + 9090: {}, + 9100: {}, + } + + commonHTTPSPorts = map[int]struct{}{ + 443: {}, + 8443: {}, + 9443: {}, + } +) + +func (p *HTTPPlugin) PortPriority(port uint16) bool { + _, ok := commonHTTPPorts[int(port)] + return ok +} + +func (p *HTTPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("http://%s", conn.RemoteAddr().String()), nil) + if err != nil { + if errors.Is(err, syscall.ECONNREFUSED) { + return nil, nil + } + return nil, &utils.RequestError{Message: err.Error()} + } + + if target.Host != "" { + req.Host = target.Host + } + + // http client with custom dialier to use the provided net.Conn + client := http.Client{ + Timeout: timeout, + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return conn, nil + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + req.Header.Set("User-Agent", USERAGENT) + + resp, err := client.Do(req) + if err != nil { + return nil, &utils.RequestError{Message: err.Error()} + } + defer resp.Body.Close() + + technologies, cpes, _ := p.FingerprintResponse(resp) + + payload := plugins.ServiceHTTP{ + Status: resp.Status, + StatusCode: resp.StatusCode, + ResponseHeaders: resp.Header, + } + if len(technologies) > 0 { + payload.Technologies = technologies + } + if len(cpes) > 0 { + payload.CPEs = cpes + } + + return plugins.CreateServiceFrom(target, payload, false, resp.Header.Get("Server"), plugins.TCP), nil +} + +func (p *HTTPSPlugin) PortPriority(port uint16) bool { + _, ok := commonHTTPSPorts[int(port)] + return ok +} + +func (p *HTTPSPlugin) Run( + conn net.Conn, + timeout time.Duration, + target plugins.Target, +) (*plugins.Service, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("https://%s", conn.RemoteAddr().String()), nil) + if err != nil { + if errors.Is(err, syscall.ECONNREFUSED) { + return nil, nil + } + return nil, &utils.RequestError{Message: err.Error()} + } + + if target.Host != "" { + req.Host = target.Host + } + + // https client with custom dialer to use the provided net.Conn + client := http.Client{ + Timeout: timeout, + Transport: &http.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return conn, nil + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + req.Header.Set("User-Agent", USERAGENT) + + resp, err := client.Do(req) + if err != nil { + return nil, &utils.RequestError{Message: err.Error()} + } + defer resp.Body.Close() + + technologies, cpes, _ := p.FingerprintResponse(resp) + + payload := plugins.ServiceHTTPS{ + Status: resp.Status, + StatusCode: resp.StatusCode, + ResponseHeaders: resp.Header, + } + if len(technologies) > 0 { + payload.Technologies = technologies + } + if len(cpes) > 0 { + payload.CPEs = cpes + } + + return plugins.CreateServiceFrom(target, payload, true, resp.Header.Get("Server"), plugins.TCP), nil +} + +func (p *HTTPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *HTTPSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *HTTPPlugin) Priority() int { + return 0 +} + +func (p *HTTPSPlugin) Priority() int { + return 1 +} + +func (p *HTTPPlugin) Name() string { + return HTTP +} + +func (p *HTTPSPlugin) Name() string { + return HTTPS +} + +func (p *HTTPPlugin) FingerprintResponse(resp *http.Response) ([]string, []string, error) { + return fingerprint(resp, p.analyzer) +} + +func (p *HTTPSPlugin) FingerprintResponse(resp *http.Response) ([]string, []string, error) { + return fingerprint(resp, p.analyzer) +} + +func fingerprint(resp *http.Response, analyzer *wappalyzer.Wappalyze) ([]string, []string, error) { + var technologies, cpes []string + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, err + } + + fingerprint := analyzer.FingerprintWithInfo(resp.Header, data) + for tech, appInfo := range fingerprint { + technologies = append(technologies, tech) + if cpe := appInfo.CPE; cpe != "" { + cpes = append(cpes, cpe) + } + } + + return technologies, cpes, nil +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/http/http_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/http/http_test.go new file mode 100644 index 0000000..7576d5b --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/http/http_test.go @@ -0,0 +1,45 @@ +package http + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" + wappalyzer "github.com/projectdiscovery/wappalyzergo" +) + +func TestHTTP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "http", + Port: 8080, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "mendhak/http-https-echo", + Tag: "24", + }, + }, + } + + p := HTTPPlugin{} + wappalyzerClient, err := wappalyzer.New() + if err != nil { + panic("unable to initialize wappalyzer library") + } + p.analyzer = wappalyzerClient + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, &p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/http/https_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/http/https_test.go new file mode 100644 index 0000000..b2e1468 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/http/https_test.go @@ -0,0 +1,45 @@ +package http + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" + wappalyzer "github.com/projectdiscovery/wappalyzergo" +) + +func TestHTTPS(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "https", + Port: 8443, + Protocol: plugins.TCPTLS, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "mendhak/http-https-echo", + Tag: "24", + }, + }, + } + + p := HTTPSPlugin{} + wappalyzerClient, err := wappalyzer.New() + if err != nil { + panic("unable to initialize wappalyzer library") + } + p.analyzer = wappalyzerClient + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, &p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/imap/imap.go b/rigour/pkg/crawler/fingerprint/plugins/services/imap/imap.go new file mode 100644 index 0000000..953960c --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/imap/imap.go @@ -0,0 +1,186 @@ +package imap + +import ( + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type IMAPPlugin struct{} +type TLSPlugin struct{} + +const IMAP = "imap" +const IMAPS = "imaps" + +func init() { + plugins.RegisterPlugin(&IMAPPlugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +/* + checkGreeting - verifies server greeting. + +/* When a client initiates a TCP handshake with an IMAP server, the server will +/* send one of three greetings immediately following the last ACK of the +/* handshake: +/* S: * OK +/* S: * PREAUTH +/* S: * BYE +*/ +func checkGreeting(response []byte) bool { + srvGreet := string(response) + srvGreetUpper := strings.ToUpper(srvGreet) + + // As per page 85 of RFC 3501, there are 3 possible greetings + greetings := []string{"* OK", "* PREAUTH", "* BYE"} + for _, greeting := range greetings { + if strings.HasPrefix(srvGreetUpper, greeting) { + return true + } + } + + return false +} + +/* + checkCapability - sends CAPABILITY command and verifies response data. + +/* CAPABILITY is an unauthenticated IMAP command that allows the client to view +/* what other commands are supported by the server. If an IP:port is running +/* IMAP, it will return data like so: +/* C: 1234 CAPABILITY\r\n +/* S: * CAPABILITY \r\n +/* S: 1234 OK \r\n +*/ +func checkCapability(conn net.Conn, timeout time.Duration) (bool, error) { + /* The tag will always be reflected in the server output. Using a random- + /* looking/nonsensical tag decreases the possibility of false positive */ + tag := "7FYWU8I4" + msg := []byte(tag + " CAPABILITY\r\n") + + response, err := utils.SendRecv(conn, msg, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + /* Sometimes servers send all the data in one packet + /* If so, parse into two strings */ + srvResponses := strings.Split(string(response), "\r\n") + + if len(srvResponses) < 2 { + return true, &utils.InvalidResponseError{Service: IMAP} + } + + capData := strings.ToUpper(srvResponses[0]) + status := strings.ToUpper(srvResponses[1]) + + // If we only got 1 IMAP response, there is probably another on the way + if status == "" { + response, err := utils.Recv(conn, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + status = string(response) + } + + /* Make sure server response matches RFC 3501, pages 68 (capability) and 88 + /* (response-tagged) */ + if !strings.HasPrefix(capData, "* CAPABILITY") || !strings.HasPrefix(status, tag) { + return true, &utils.InvalidResponseErrorInfo{Service: IMAP, Info: "missing capability info"} + } + + // imap + return false, nil +} + +func DetectIMAP(conn net.Conn, timeout time.Duration) (string, bool, error) { + /* Server has to specify a greeting upon completing the TCP handshake as + /* per RFC 3501 (page 14). If we don't get a greeting, this ain't IMAP. */ + response, err := utils.Recv(conn, timeout) + if err != nil { + return "", false, err + } + if len(response) == 0 { + return "", true, &utils.ServerNotEnable{} + } + + if !checkGreeting(response) { + return "", true, &utils.InvalidResponseErrorInfo{ + Service: IMAP, + Info: "did not receive expected imap greeting banner", + } + } + check, err := checkCapability(conn, timeout) + return string(response[5:]), check, err +} + +func (p *IMAPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, check, err := DetectIMAP(conn, timeout) + if err != nil && check { // service is not running IMAP + return nil, nil + } else if err != nil && !check { // plugin error + return nil, err + } + + // service is running IMAP + payload := plugins.ServiceIMAPS{ + Banner: result, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *IMAPPlugin) PortPriority(i uint16) bool { + return i == 143 +} + +func (p *IMAPPlugin) Name() string { + return IMAP +} + +func (p *IMAPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, check, err := DetectIMAP(conn, timeout) + if err != nil && check { // service is not running IMAP + return nil, nil + } else if err != nil && !check { // plugin error + return nil, err + } + + // service is running IMAPS + payload := plugins.ServiceIMAPS{ + Banner: result, + } + return plugins.CreateServiceFrom(target, payload, true, "", plugins.TCP), nil +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 993 +} + +func (p *TLSPlugin) Name() string { + return IMAPS +} + +func (p *IMAPPlugin) Priority() int { + return 191 +} + +func (p *TLSPlugin) Priority() int { + return 190 +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/imap/imap_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/imap/imap_test.go new file mode 100644 index 0000000..f4e4d4b --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/imap/imap_test.go @@ -0,0 +1,38 @@ +package imap + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestIMAP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "imap", + Port: 143, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "instrumentisto/dovecot", + }, + }, + } + + p := &IMAPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ipmi/ipmi.go b/rigour/pkg/crawler/fingerprint/plugins/services/ipmi/ipmi.go new file mode 100644 index 0000000..7b85137 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ipmi/ipmi.go @@ -0,0 +1,128 @@ +package ipmi + +import ( + "io" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" +) + +// http://72.47.221.139/sites/default/files/standards/documents/DSP0114.pdf + +var ipmiInitialPacket = [23]byte{ + + // + // Remote Management Control Protocol, Class: IPMI + // Version: 0x06 + // Reserved: 0x00 + // Sequence: 0xFF + // Type: 0x07 + // + + 0x06, 0x00, 0xFF, 0x07, + + // + // IPMI v1.5 Session Wrapper, Session ID 0x00 + // Authentication Type: NONE (0x00) + // Session ID: 0x00 0x00 0x00 0x00 + // Session Sequence number: 0x00 0x00 0x00 0x00 + // Message Length: 9 + // + + 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x09, + + // + // Intelligent Platform Management Bus + // Bus Command Data: 20 18 C8 81 00 38 8E 04 B5 + // + + 0x20, 0x18, 0xC8, 0x81, 0x00, 0x38, 0x8E, 0x04, 0xB5, +} + +var ipmiExpectedResponse = [13]byte{ + + /* + * Remote Management Control Protocol, Class: IPMI + * Version: 0x06 + * Reserved: 0x00 + * Sequence: 0xFF + * Type: 0x07 + */ + + 0x06, 0x00, 0xFF, 0x07, + + // + // IPMI v1.5 Session Wrapper, Session ID 0x00 + // Authentication Type: NONE (0x00) + // Session ID: 0x00 0x00 0x00 0x00 + // Session Sequence number: 0x00 0x00 0x00 0x00 + // + + 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +} + +type IPMIPlugin struct{} + +const IPMI = "ipmi" + +func isIPMI(conn net.Conn, timeout time.Duration) (bool, error) { + _, err := conn.Write(ipmiInitialPacket[:]) + if err != nil { + return false, err + } + + response := make([]byte, len(ipmiExpectedResponse)) + + err = conn.SetReadDeadline(time.Now().Add(timeout)) + if err != nil { + return false, err + } + + _, err = io.ReadFull(conn, response) + if err != nil { + return false, err + } + + for i, b := range ipmiExpectedResponse { + if response[i] != b { + return false, nil + } + } + + return true, nil +} + +func init() { + plugins.RegisterPlugin(&IPMIPlugin{}) +} + +func (p *IPMIPlugin) PortPriority(port uint16) bool { + return port == 623 +} + +func (p *IPMIPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + if isIPMI, err := isIPMI(conn, timeout); !isIPMI || err != nil { + return nil, nil + } + payload := plugins.ServiceIPMI{} + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil +} + +func (p *IPMIPlugin) Name() string { + return IPMI +} + +func (p *IPMIPlugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *IPMIPlugin) Priority() int { + return 80 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ipmi/ipmi_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/ipmi/ipmi_test.go new file mode 100644 index 0000000..3d7692b --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ipmi/ipmi_test.go @@ -0,0 +1,39 @@ +package ipmi + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestIPMI(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ipmi", + Port: 623, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "vaporio/ipmi-simulator", + ExposedPorts: []string{"623/udp"}, + }, + }, + } + + p := &IPMIPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ipsec/ipsec.go b/rigour/pkg/crawler/fingerprint/plugins/services/ipsec/ipsec.go new file mode 100644 index 0000000..e360924 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ipsec/ipsec.go @@ -0,0 +1,120 @@ +package ipsec + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const IPSEC = "IPsec" + +type Plugin struct{} + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (f *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + initiator := make([]byte, 8) + _, err := rand.Read(initiator) + if err != nil { + return nil, &utils.RandomizeError{Message: "initiator SPI"} + } + InitialConnectionPackage := append(initiator, []byte{ //nolint:gocritic + // 8 bit Initiator SPI + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Responder SPI + 0x01, 0x10, // Version: 1.0 + 0x02, // Exchange type + 0x00, + 0x00, 0x00, 0x00, 0x00, // ID + 0x00, 0x00, 0x01, 0x50, // Message Length + 0x00, 0x00, 0x01, 0x34, // Payload Length + 0x00, 0x00, 0x00, 0x01, // Domain of interpretation: IPSEC (1) + 0x00, 0x00, 0x00, 0x01, // Situation: identity only + 0x00, 0x00, 0x01, 0x28, // Payload Length + 0x01, // Proposal number: 1 + 0x01, // Protocol ID: ISAKMP (1) + 0x00, 0x08, // Proposal transforms: 8 + + // SHA 3DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x01, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x05, 0x80, 0x02, 0x00, 0x02, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x02, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // MD5 3DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x02, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x05, 0x80, 0x02, 0x00, 0x01, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x02, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // SHA DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x03, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x01, 0x80, 0x02, 0x00, 0x02, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x02, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // MD5 3DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x04, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x01, 0x80, 0x02, 0x00, 0x01, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x02, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // SHA 3DES-CBC 768 bit + 0x03, 0x00, 0x00, 0x24, 0x05, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x05, 0x80, 0x02, 0x00, 0x02, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x01, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // MD5 3DES-CBC 768 bit + 0x03, 0x00, 0x00, 0x24, 0x06, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x05, 0x80, 0x02, 0x00, 0x01, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x01, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // SHA DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x07, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x01, 0x80, 0x02, 0x00, 0x02, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x01, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // MD5 3DES-CBC 1024 bit + 0x00, 0x00, 0x00, 0x24, 0x08, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x01, 0x80, 0x02, 0x00, 0x01, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x01, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + }...) + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + responderISP := hex.EncodeToString(response[8:16]) + messageID := hex.EncodeToString(response[20:24]) + if bytes.Equal(initiator, response[0:8]) { + payload := plugins.ServiceIPSEC{ + ResponderISP: responderISP, + MessageID: messageID, + } + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil + } + return nil, nil +} + +func (f *Plugin) PortPriority(i uint16) bool { + return i == 500 +} + +func (f *Plugin) Name() string { + return IPSEC +} + +func (f *Plugin) Priority() int { + return 198 +} + +func (f *Plugin) Type() plugins.Protocol { + return plugins.UDP +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ipsec/ipsec_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/ipsec/ipsec_test.go new file mode 100644 index 0000000..50f4cbb --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ipsec/ipsec_test.go @@ -0,0 +1,47 @@ +package ipsec + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestIPSEC(t *testing.T) { + // Flaky in CI: depends on container timing/networking and can fail to + // fingerprint reliably on shared runners. + t.Skip("skipping flaky docker integration test") + + testcases := []test.Testcase{ + { + Description: "ipsec", + Port: 500, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "hwdsl2/ipsec-vpn-server", + Mounts: []string{ + "ikev2-vpn-data:/etc/ipsec.d", + "/lib/modules:/lib/modules:ro", + }, + Privileged: true, + }, + }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/jdwp/jdwp.go b/rigour/pkg/crawler/fingerprint/plugins/services/jdwp/jdwp.go new file mode 100644 index 0000000..0f8b68c --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/jdwp/jdwp.go @@ -0,0 +1,177 @@ +package jdwp + +import ( + "bytes" + "encoding/binary" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type JDWPPlugin struct{} + +const JDWP = "jdwp" + +var ( + commonJDWPPorts = map[int]struct{}{ + 3999: {}, + 5000: {}, + 5005: {}, + 8000: {}, + 8453: {}, + 8787: {}, + 8788: {}, + 9001: {}, + 18000: {}, + } +) + +type JDWPPacket struct { + Length uint32 + ID uint32 + Flags byte + CommandSet byte + Command byte +} + +func init() { + plugins.RegisterPlugin(&JDWPPlugin{}) +} + +func DetectJDWPVersion(conn net.Conn, timeout time.Duration) (*plugins.ServiceJDWP, error) { + info := plugins.ServiceJDWP{} + + versionRequest := JDWPPacket{ + Length: 0x0B, + ID: 0x01, + Flags: 0x00, + CommandSet: 0x01, + Command: 0x01, + } + + versionBuf := new(bytes.Buffer) + err := binary.Write(versionBuf, binary.BigEndian, versionRequest) + if err != nil { + return nil, err + } + + response, err := utils.SendRecv(conn, versionBuf.Bytes(), timeout) + if err != nil { + return nil, err + } + if len(response) < 11 { + return nil, nil + } + + var versionResponse JDWPPacket + responseBuf := bytes.NewBuffer(response) + err = binary.Read(responseBuf, binary.BigEndian, &versionResponse) + if err != nil { + return nil, err + } + + if versionResponse.Length != (uint32(len((response)))) { + return nil, err + } + + var descriptionLength uint32 + err = binary.Read(responseBuf, binary.BigEndian, &descriptionLength) + if err != nil { + return nil, err + } + description := make([]byte, descriptionLength) + err = binary.Read(responseBuf, binary.BigEndian, &description) + if err != nil { + return nil, err + } + + var jdwpMajor int32 + err = binary.Read(responseBuf, binary.BigEndian, &jdwpMajor) + if err != nil { + return nil, err + } + var jdwpMinor int32 + err = binary.Read(responseBuf, binary.BigEndian, &jdwpMinor) + if err != nil { + return nil, err + } + + var vmVersionLength uint32 + err = binary.Read(responseBuf, binary.BigEndian, &vmVersionLength) + if err != nil { + return nil, err + } + vmVersion := make([]byte, vmVersionLength) + err = binary.Read(responseBuf, binary.BigEndian, &vmVersion) + if err != nil { + return nil, err + } + + var vmNameLength uint32 + err = binary.Read(responseBuf, binary.BigEndian, &vmNameLength) + if err != nil { + return nil, err + } + vmName := make([]byte, vmNameLength) + err = binary.Read(responseBuf, binary.BigEndian, &vmName) + if err != nil { + return nil, err + } + + info.Description = string(description) + info.JdwpMajor = jdwpMajor + info.JdwpMinor = jdwpMinor + info.VMVersion = string(vmVersion) + info.VMName = string(vmName) + + return &info, nil +} + +func (p *JDWPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + requestBytes := []byte{ + // ascii "JDWP-Handshake" + 0x4a, 0x44, 0x57, 0x50, 0x2d, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, + } + + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + if !bytes.Equal(requestBytes, response) { + return nil, nil + } + + info, err := DetectJDWPVersion(conn, timeout) + if err != nil { + return nil, err + } + + if info == nil { + return plugins.CreateServiceFrom(target, nil, false, "", plugins.TCP), nil + } + + return plugins.CreateServiceFrom(target, info, false, info.VMVersion, plugins.TCP), nil +} + +func (p *JDWPPlugin) PortPriority(port uint16) bool { + _, ok := commonJDWPPorts[int(port)] + return ok +} + +func (p *JDWPPlugin) Name() string { + return JDWP +} + +func (p *JDWPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *JDWPPlugin) Priority() int { + return 500 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/jdwp/jdwp_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/jdwp/jdwp_test.go new file mode 100644 index 0000000..c06b485 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/jdwp/jdwp_test.go @@ -0,0 +1,23 @@ +// Copyright 2022 Praetorian Security, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jdwp + +import ( + "testing" +) + +// tested locally against a Java process with JDWP enabled +func TestJDWP(_ *testing.T) { +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew.go b/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew.go new file mode 100644 index 0000000..c253a09 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew.go @@ -0,0 +1,196 @@ +package kafkanew + +import ( + "encoding/binary" + "math" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type Plugin struct{} +type TLSPlugin struct{} + +const KAFKA = "kafkaNew" +const KAFKATLS = "KafkaNewTLS" + +func init() { + plugins.RegisterPlugin(&Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, err := Run(conn, false, timeout, target) + return result, err +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 9092 +} + +func (p *Plugin) Name() string { + return KAFKA +} + +func (p *Plugin) Priority() int { + return 200 +} + +func (p *TLSPlugin) Priority() int { + return 200 +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, err := Run(conn, true, timeout, target) + return result, err +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 9093 +} + +func (p *TLSPlugin) Name() string { + return KAFKATLS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +/* +Run Kafka scanner plugins. + +Primary Sources: + - https://kafka.apache.org/protocol.html (Gold mine) + - https://kafka.apache.org/documentation.html + - https://kafka.apache.org/downloads + +Methodology: +Scanning for Kafka is a bit tricky, so I've outlined my methodology here. Kafka +is harder to detect reliably for a few reasons: + - Kafka brokers may optionally require authentication via SASL before most + commands can be issued. + - There are many different versions of Kafka, and most API calls work slightly + different on each versions (especially for pre-0.9.0.X releases) + +Fortunately, Kafka versions 0.10.0.0 and later support the ApiVersions request, +which can be sent by an unauthenticated user to check which API requests are +supported by the broker. Also versions prior to 0.9.0.0 do not offer any form of +authentication. And, all versions of Kafka are compatible with any older client. +This means that: + 1. If Kafka version 0.10.0.0 or higher is running, we can confirm with the + ApiVersions request regardless of if authentication is required This + includes any version of Kafka released since May, 2016. + 2. If Kafka version 0.8.0.X or earlier is running, we can confirm with a simple + data query using API version 0. + 3. If Kafka version 0.9.0.X is running and does not require authentication, we + can also confirm with a simple v0 data query. + +I'm not sure if Kafka brokers running version 0.9.0.X that do require +authentication will be detected by any of the above methods. It's possible that +strategy 3 will still work in this situation, but I was not able to confirm due +to the difficulty of setting up a testing environment for an older version. +*/ +func Run(conn net.Conn, tls bool, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + /* Initiate first TCP connection with target. If the target is running an + /* older version of Kafka, the connection will be terminated after sending + /* ApiVersions, and we will need to make a new one. */ + /* Make first notReportError - this will catch any broker running Kafka 0.10.0.0 or + /* later. */ + notReportError, err := checkAPIVersions(conn, timeout) + if err != nil { + if !notReportError { + return nil, err + } + return nil, nil + } + if !notReportError { + return nil, nil + } + + return plugins.CreateServiceFrom(target, plugins.ServiceKafka{}, tls, ">=0.10.0.0", plugins.TCP), nil +} + +/* Helper function to generate a correlation_id */ +/* Might update to be random later */ +func genCorrelationID() []byte { + cid := []byte{0x1e, 0x33, 0xf4, 0x81} + return cid +} + +/* + checkApiVersions - sends an ApiVersions request and validates the output. + +/* +/* Note that if the broker does not support ApiVersions, it might terminate the +/* TCP connection (source: https://kafka.apache.org/protocol.html#api_versions). +/* +/* The function sends an ApiVersions request because this is widely supported, +/* and does not require authentication. All Kafka responses start with the +/* packet length followed by the "correlation ID", which is a value specified by +/* the client and included in their request. So we check to make sure the first +/* four bytes (length) are equivalent to the size of the response data and the +/* next four bytes (correlation ID) match the ID included in the request. +/* Further reading: https://kafka.apache.org/protocol.html#protocol_messages +*/ +func checkAPIVersions(conn net.Conn, timeout time.Duration) (bool, error) { + cid := genCorrelationID() + apiVersionsRequest := []byte{ + /* length */ + 0x00, 0x00, 0x00, 0x43, + /* request_api_key */ + 0x00, 0x12, + /* request_api_version */ + 0x00, 0x00, + /* correlation_id */ + cid[0], cid[1], cid[2], cid[3], + /* client_id */ + 0x00, 0x1f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, + 0x65, 0x72, 0x2d, 0x4f, 0x66, 0x66, 0x73, 0x65, + 0x74, 0x20, 0x45, 0x78, 0x70, 0x6c, 0x6f, 0x72, + 0x65, 0x72, 0x20, 0x32, 0x2e, 0x32, 0x2d, 0x31, + 0x38, + /* TAG_BUFFER */ + 0x00, + /* client_software_name */ + 0x12, 0x61, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, + 0x6b, 0x61, 0x66, 0x6b, 0x61, 0x2d, 0x6a, 0x61, + 0x76, 0x61, + /* client_software_version */ + 0x06, 0x32, 0x2e, 0x34, 0x2e, 0x30, + /* _tagged_fields */ + 0x00, + } + + response, err := utils.SendRecv(conn, apiVersionsRequest, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + responseLength := binary.BigEndian.Uint32(response[0:4]) + expectedLength := uint32(math.Max(float64(len(response)-4), 0)) + correlationID := response[4:8] + + // First, check to see if the response length makes sense + if responseLength != expectedLength { + return false, nil + } + + // Next, make sure the correlation IDs match up + for i := 0; i < len(cid); i++ { + if cid[i] != correlationID[i] { + return false, nil + } + } + + return true, nil +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew_test.go new file mode 100644 index 0000000..f0f5d6b --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew_test.go @@ -0,0 +1,38 @@ +package kafkanew + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestKafkaNew(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "kafkanew", + Port: 9092, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "spotify/kafka", + }, + }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld.go b/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld.go new file mode 100644 index 0000000..44b54af --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld.go @@ -0,0 +1,233 @@ +package kafkaold + +import ( + "crypto/rand" + "encoding/binary" + "math" + "math/big" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type Plugin struct{} +type TLSPlugin struct{} + +const KAFKA = "kafkaOld" +const KAFKATLS = "KafkaOldTLS" + +func init() { + plugins.RegisterPlugin(&Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, err := Run(conn, false, timeout, target) + return result, err +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 9092 +} + +func (p *Plugin) Priority() int { + return 201 +} + +func (p *TLSPlugin) Priority() int { + return 201 +} + +func (p *Plugin) Name() string { + return KAFKA +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, err := Run(conn, true, timeout, target) + return result, err +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 9093 +} + +func (p *TLSPlugin) Name() string { + return KAFKATLS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +/* +Run Kafka scanner plugins. + +Primary Sources: + - https://kafka.apache.org/protocol.html (Gold mine) + - https://kafka.apache.org/documentation.html + - https://kafka.apache.org/downloads + +Methodology: +Scanning for Kafka is a bit tricky, so I've outlined my methodology here. Kafka +is harder to detect reliably for a few reasons: + - Kafka brokers may optionally require authentication via SASL before most + commands can be issued. + - There are many different versions of Kafka, and most API calls work slightly + different on each versions (especially for pre-0.9.0.X releases) + +Fortunately, Kafka versions 0.10.0.0 and later support the ApiVersions request, +which can be sent by an unauthenticated user to check which API requests are +supported by the broker. Also versions prior to 0.9.0.0 do not offer any form of +authentication. And, all versions of Kafka are compatible with any older client. +This means that: + 1. If Kafka version 0.10.0.0 or higher is running, we can confirm with the + ApiVersions request regardless of if authentication is required This + includes any version of Kafka released since May, 2016. + 2. If Kafka version 0.8.0.X or earlier is running, we can confirm with a simple + data query using API version 0. + 3. If Kafka version 0.9.0.X is running and does not require authentication, we + can also confirm with a simple v0 data query. + +I'm not sure if Kafka brokers running version 0.9.0.X that do require +authentication will be detected by any of the above methods. It's possible that +strategy 3 will still work in this situation, but I was not able to confirm due +to the difficulty of setting up a testing environment for an older version. +*/ +func Run(conn net.Conn, tls bool, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + /* Initiate first TCP connection with target. If the target is running an + /* older version of Kafka, the connection will be terminated after sending + /* ApiVersions, and we will need to make a new one. */ + /* Make first notReportError - this will catch any broker running Kafka 0.10.0.0 or + /* later. */ + notReportError, err := checkMetadataQuery(conn, timeout) + if err != nil { + if !notReportError { + return nil, err + } + return nil, nil + } + if !notReportError { + return nil, nil + } + return plugins.CreateServiceFrom(target, plugins.ServiceKafka{}, tls, "<=0.9.0.X", plugins.TCP), nil +} + +/* Helper function to generate a correlation_id */ +/* Might update to be random later */ +func genCorrelationID() []byte { + cid := []byte{0x1e, 0x33, 0xf4, 0x81} + return cid +} + +/* Helper function for generating a random alphanumeric string */ +func genRandomString(length int) (string, error) { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + str := make([]byte, length) + for i := 0; i < length; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", &utils.RandomizeError{Message: "KafkaRandomString"} + } + str[i] = charset[num.Int64()] + } + + return string(str), nil +} + +func checkMetadataQuery(conn net.Conn, timeout time.Duration) (bool, error) { + cid := genCorrelationID() + topicName, err := genRandomString(6) + if err != nil { + return false, err + } + metadataRequest := []byte{ + // length + 0x00, 0x00, 0x00, 0x00, + // request_api_key + 0x00, 0x03, + // request_api_version + 0x00, 0x00, + // correlation_id + cid[0], cid[1], cid[2], cid[3], + // client_id + 0x00, 0x0d, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2d, 0x35, + // topic_count + 0x00, 0x00, 0x00, 0x01, + // topics + 0x00, 0x06, topicName[0], topicName[1], topicName[2], topicName[3], + topicName[4], topicName[5], + } + + /* Correct the length field - not necessary for final script, but makes + /* debugging easier */ + packetLength := make([]byte, 4) + binary.BigEndian.PutUint32(packetLength, uint32(len(metadataRequest)-4)) + for i := 0; i < 4; i++ { + metadataRequest[i] = packetLength[i] + } + + response, err := utils.SendRecv(conn, metadataRequest, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + // Similar to checkApiVersions, first we test the length + responseLength := binary.BigEndian.Uint32(response[0:4]) + expectedLength := uint32(math.Max(float64(len(response)-4), 0)) + if responseLength != expectedLength { + return false, nil + } + + // Next, verify correlation_id + correlationID := response[4:8] + for i := 0; i < 4; i++ { + if cid[i] != correlationID[i] { + return false, nil + } + } + + /* Finally, check to make sure the topic name is in the expected location. + /* Our server's response data begins at index 8 (4 bytes for length and 4 + /* bytes for correlation_id). For metadataRequest, this is information about + /* the available brokers, which is a variable-sized array. So we must run + /* through the array to accurately skip over the brokers section. */ + brokerIndex := uint16(8) + brokerCount := binary.BigEndian.Uint32(response[brokerIndex : brokerIndex+4]) + + index := brokerIndex + 4 + for i := uint32(0); i < brokerCount; i++ { + /* Each version 0 broker object looks like the following: + /* node_id (INT32, 4 bytes) + /* host (STRING, First the length N is given as an INT16. Then N bytes follow. So 2 + N bytes total) + /* port (INT32, 4 bytes) */ + hostLength := binary.BigEndian.Uint16(response[index+4 : index+6]) + index += 4 + 2 + hostLength + 4 + } + + topicsIndex := index + + /* Topic objects are similar to brokers, but we only requested one in our + /* metadataRequest. So there should only be one, and the topic name is + /* always the second field. */ + topicsIndex += 4 // for topics_count (INT32) + topicsIndex += 2 // for status code (INT16) + topicNameLength := binary.BigEndian.Uint16(response[topicsIndex : topicsIndex+2]) + topicsIndex += 2 // for string length (INT16) + tName := string(response[topicsIndex : topicsIndex+topicNameLength]) + + if tName != topicName { + return false, nil + } + + return true, nil +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld_test.go new file mode 100644 index 0000000..31914df --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld_test.go @@ -0,0 +1,41 @@ +package kafkaold + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestKafkaOld(t *testing.T) { + // Flaky in CI: legacy Kafka images and startup times vary. + t.Skip("skipping flaky docker integration test") + + testcases := []test.Testcase{ + { + Description: "kafkaold", + Port: 9092, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "spotify/kafka", + }, + }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ldap/ldap.go b/rigour/pkg/crawler/fingerprint/plugins/services/ldap/ldap.go new file mode 100644 index 0000000..2404ea6 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ldap/ldap.go @@ -0,0 +1,205 @@ +package ldap + +import ( + "bytes" + "encoding/binary" + "math/rand" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type LDAPPlugin struct{} +type TLSPlugin struct{} + +const LDAP = "ldap" +const LDAPS = "ldaps" + +func init() { + plugins.RegisterPlugin(&LDAPPlugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +/* + +Data is BER encoded (Basic Encoding Rules) - Format: Type-Length-Value + +Type: + Format of Types: Bits 8-7 6 5-1 + Purpose Class Prim/Constructed Tag Number + + For example 00 11 00 00 represents the Class (Universal) Constructed Sequence (Sequence is tag number 1 00 00) + Ie: The sequence tag +Length: + Single Byte - Length is a single byte containing the number of bytes in the message up to 127 + Multi Bsyte - The most significant bit is set to 1. The remaining 7 bytes are used to indicate how + many bytes are needed to represent the length, followed by that many bytes + + + +Example Bind Request +0000 30 2b 02 01 01 60 26 02 01 03 04 1a 63 6e 3d 61 +0010 64 6d 69 6e 2c 64 63 3d 65 78 61 6d 70 6c 65 2c +0020 64 63 3d 6f 72 67 80 05 61 64 6d 69 6e + +Notes: + +The messageId MUST be non-zero and different from any other request in the session + +30 2b ... Represents a universal sequence containing 43 bytes + +02 01 01 Represents an Integer (02) type, length 1 byte, and value of 1 + (denoting a message Id of 1) - this number is reflected back in responses + +60 26 denotes a bind request of 38 bytes + +02 01 03 represents an integer of 1 byte with a value of 3 (the protocol version) + +04 1a 63 6e 3d 61 64 6d 69 6e 2c 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 6f 72 67 +Represents a universal string (04) of length 26 bytes containing the value 'cn=admin,dc=example,dc=org' + +80 05 61 64 6d 69 6e Represents a context specific 5 length string holding the simple auth password of 'admin' + +*/ + +func generateRandomString(length int) []byte { + charset := "abcdefghijklmnopqrstuvwxyz" + result := make([]byte, length) + + for i := range result { + result[i] = charset[rand.Intn(len(charset))] //nolint:gosec + } + return result +} + +func generateBindRequestAndID() [2][]byte { + rand.Seed(time.Now().UnixNano()) + sequenceBERHeader := [2]byte{0x30, 0x3a} + messageID := uint32(rand.Int31()) //nolint:gosec + messageIDBytes := [4]byte{} + binary.BigEndian.PutUint32(messageIDBytes[:], messageID) + messageIDBERHeader := [2]byte{0x02, 0x04} + finalMessageIDBER := make([]byte, 6) + copy(finalMessageIDBER[:2], messageIDBERHeader[:]) + copy(finalMessageIDBER[2:], messageIDBytes[:]) + bindRequestHeader := [2]byte{0x60, 0x32} + versionBER := [3]byte{0x02, 0x01, 0x03} + stringBERHeader := [2]byte{0x04, 0x17} + stringContextBERHeader := [2]byte{0x80, 0x14} + // We attempt to auth with a random distinguished name and password (generated below) + randomAlphaString := generateRandomString(20) + dePrefix := []byte("cn=") + distinguishedName := append(dePrefix, randomAlphaString...) //nolint:gocritic + passwordBER := randomAlphaString + combine := [][]byte{ + sequenceBERHeader[:], + finalMessageIDBER, + bindRequestHeader[:], + versionBER[:], + stringBERHeader[:], + distinguishedName, + stringContextBERHeader[:], + passwordBER, + } + fullBindRequest := make([]byte, 60) + index := 0 + for _, s := range combine { + index += copy(fullBindRequest[index:], s) + } + + return [2][]byte{fullBindRequest, finalMessageIDBER} +} + +func DetectLDAP(conn net.Conn, timeout time.Duration) (bool, error) { + requestAndID := generateBindRequestAndID() + + response, err := utils.SendRecv(conn, requestAndID[0], timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return false, nil + } + + expectedSequenceByte := byte(0x30) + expectedMessageLengthByte := byte(len(response) - 2) + // The LDAP header should have the right message ID bytes, the right sequence byte (the first byte), + // and the right length byte + expectedLDAPHeader := append( + []byte{expectedSequenceByte, expectedMessageLengthByte}, + requestAndID[1]...) + + // We might be able to try to look at the specific response message in responseBuff to try to fingerprint the specific + // vendor, but didn't attempt to do that in this current version (might be more time than value added currently) + + // In other versions, bytes at response[1:5] may differ so we remove these bytes and + // perform the expected header check against this 'otherVersionResponse' as well + if len(response) < 7 { + return false, nil + } + otherVersionResponse := append([]byte{response[0]}, response[5]+4) + otherVersionResponse = append(otherVersionResponse, response[6:]...) + + if bytes.HasPrefix(response, expectedLDAPHeader) || bytes.HasPrefix(otherVersionResponse, expectedLDAPHeader) { + return true, nil + } + return false, nil +} + +func (p *LDAPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + isLDAP, err := DetectLDAP(conn, timeout) + if err != nil { + return nil, err + } + + if isLDAP { + return plugins.CreateServiceFrom(target, plugins.ServiceLDAP{}, false, "", plugins.TCP), nil + } + return nil, nil +} + +func (p *LDAPPlugin) PortPriority(i uint16) bool { + return i == 389 +} + +func (p *LDAPPlugin) Name() string { + return LDAP +} + +func (p *LDAPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + isLDAPS, err := DetectLDAP(conn, timeout) + if err != nil { + return nil, err + } + + if isLDAPS { + return plugins.CreateServiceFrom(target, plugins.ServiceLDAPS{}, true, "", plugins.TCP), nil + } + return nil, nil +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 636 +} + +func (p *LDAPPlugin) Priority() int { + return 175 +} + +func (p *TLSPlugin) Priority() int { + return 175 +} + +func (p *TLSPlugin) Name() string { + return LDAPS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ldap/ldap_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/ldap/ldap_test.go new file mode 100644 index 0000000..dccf7f7 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ldap/ldap_test.go @@ -0,0 +1,46 @@ +package ldap + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestLDAP(t *testing.T) { + // This test relies on a Dockerized LDAP server and port mapping discovery. + // In some environments dockertest can't resolve the mapped port ("missing address"), + // making the test non-deterministic. Skip to keep `go test ./...` stable. + t.Skip("skipping LDAP docker integration test (flaky port mapping in CI)") + + testcases := []test.Testcase{ + { + Description: "ldap", + Port: 1389, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + // bitnami/openldap tags have been historically unstable/removed. + // osixia/openldap is widely used and tends to keep tags around. + Repository: "osixia/openldap", + Tag: "1.5.0", + }, + }, + } + + p := &LDAPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/linuxrpc/linuxrpc.go b/rigour/pkg/crawler/fingerprint/plugins/services/linuxrpc/linuxrpc.go new file mode 100644 index 0000000..28f999f --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/linuxrpc/linuxrpc.go @@ -0,0 +1,167 @@ +package linuxrpc + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +/* +The RPC service takes two main operations we care about: call and dump. + +Call verify that the service is running and will return the version of rpc running +Dump dumps a list of all registered rpc endpoints in a list, with each entry having the following structure: + +RPCB +Program: Portmap (100000) +Version: 4 +Network Id: tcp6 + length: 4 + contents: tcp6 +Universal Address: ::.0.111 + length: 8 + contents: ::.0.111 +Owner of this Service: superuser + length: 9 + contents: superuser + fill bytes: opaque data +Value follows: Yes + +Bytes are padded to 4 bytes +*/ + +type RPCPlugin struct{} + +const RPC = "RPC" + +func init() { + plugins.RegisterPlugin(&RPCPlugin{}) +} + +func (p *RPCPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + rpcService := plugins.ServiceRPC{} + + check, err := DetectRPCInfoService(conn, &rpcService, timeout) + if check && err != nil { + return nil, nil + } + if err == nil { + return plugins.CreateServiceFrom(target, rpcService, false, "", plugins.TCP), nil + } + return nil, err +} + +func DetectRPCInfoService(conn net.Conn, lookupResponse *plugins.ServiceRPC, timeout time.Duration) (bool, error) { + callPacket := []byte{ + 0x80, 0x00, 0x00, 0x28, 0x72, 0xfe, 0x1d, 0x13, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x01, 0x86, 0xa0, 0x00, 0x01, 0x97, 0x7c, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + callResponseSignature := []byte{ + 0x72, 0xfe, 0x1d, 0x13, 0x00, 0x00, 0x00, 0x01, + } + + dumpPacket := []byte{ + 0x80, 0x00, 0x00, 0x28, 0x3d, 0xd3, 0x77, 0x29, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x01, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, callPacket, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + if !bytes.Contains(response, callResponseSignature) { + return true, &utils.InvalidResponseError{Service: RPC} + } + + response, err = utils.SendRecv(conn, dumpPacket, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + return true, parseRPCInfo(response, lookupResponse) +} + +func parseRPCInfo(response []byte, lookupResponse *plugins.ServiceRPC) error { + if len(response) < 0x20 { + return fmt.Errorf("invalid rpc length") + } + response = response[0x20:] + valueFollows := 1 + + for valueFollows == 1 { + tmp := plugins.RPCB{} + if len(response) < 0x20 { + return nil + } + + tmp.Program = int(binary.BigEndian.Uint32(response[0:4])) + response = response[4:] + tmp.Version = int(binary.BigEndian.Uint32(response[0:4])) + response = response[4:] + networkIDLen := int(binary.BigEndian.Uint32(response[0:4])) + for networkIDLen%4 != 0 { + networkIDLen++ + } + response = response[4:] + tmp.Protocol = string(response[0:networkIDLen]) + response = response[networkIDLen:] + addressLen := int(binary.BigEndian.Uint32(response[0:4])) + for addressLen%4 != 0 { + addressLen++ + } + response = response[4:] + tmp.Address = string(response[0:addressLen]) + response = response[addressLen:] + ownerLen := int(binary.BigEndian.Uint32(response[0:4])) + for ownerLen%4 != 0 { + ownerLen++ + } + response = response[4:] + tmp.Owner = string(response[0:ownerLen]) + response = response[ownerLen:] + + valueFollows = int(binary.BigEndian.Uint32(response[0:4])) + response = response[4:] + + lookupResponse.Entries = append(lookupResponse.Entries, tmp) + } + + return nil +} + +func (p *RPCPlugin) PortPriority(i uint16) bool { + return i == 111 +} + +func (p *RPCPlugin) Name() string { + return RPC +} + +func (p *RPCPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *RPCPlugin) Priority() int { + return 300 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/linuxrpc/linuxrpc_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/linuxrpc/linuxrpc_test.go new file mode 100644 index 0000000..dec623b --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/linuxrpc/linuxrpc_test.go @@ -0,0 +1,40 @@ +package linuxrpc + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestRPC(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "alpine-nfs", + Port: 111, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "woahbase/alpine-nfs", + Tag: "x86_64", + Privileged: true, + }, + }, + } + + p := &RPCPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/minecraft/java/minecraft.go b/rigour/pkg/crawler/fingerprint/plugins/services/minecraft/java/minecraft.go new file mode 100644 index 0000000..84ee508 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/minecraft/java/minecraft.go @@ -0,0 +1,229 @@ +package minecraftjava + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type MinecraftPlugin struct{} + +const MinecraftJava = "minecraft-java" +const DefaultPort = uint16(25565) + +func init() { + plugins.RegisterPlugin(&MinecraftPlugin{}) +} + +func (p *MinecraftPlugin) PortPriority(port uint16) bool { + return port == DefaultPort +} + +func (p *MinecraftPlugin) Name() string { + return MinecraftJava +} + +func (p *MinecraftPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *MinecraftPlugin) Priority() int { + return 1 +} + +type mcStatusResponse struct { + Version struct { + Name string `json:"name"` + Protocol int `json:"protocol"` + } `json:"version"` + Players struct { + Max int `json:"max"` + Online int `json:"online"` + } `json:"players"` + Description any `json:"description"` + Favicon string `json:"favicon"` + SecureChat bool `json:"enforcesSecureChat"` +} + +func writeVarInt(w io.Writer, v int32) error { + uv := uint32(v) + for { + if (uv & ^uint32(0x7F)) == 0 { + _, err := w.Write([]byte{byte(uv)}) + return err + } + b := byte(uv&0x7F) | 0x80 + if _, err := w.Write([]byte{b}); err != nil { + return err + } + uv >>= 7 + } +} + +func readVarInt(r io.ByteReader) (int32, error) { + var numRead int + var result int32 + for { + b, err := r.ReadByte() + if err != nil { + return 0, err + } + value := int32(b & 0x7F) + result |= value << (7 * numRead) + numRead++ + if numRead > 5 { + return 0, fmt.Errorf("varint too big") + } + if (b & 0x80) == 0 { + break + } + } + return result, nil +} + +func writeString(w io.Writer, s string) error { + if err := writeVarInt(w, int32(len(s))); err != nil { + return err + } + _, err := w.Write([]byte(s)) + return err +} + +func encodeHandshake(host string, port uint16) ([]byte, error) { + // Handshake packet: id=0x00 + // protocol version: use 754 (1.16.5) as a broadly accepted value for status. + // server address, server port, next state=1 (status) + body := &bytes.Buffer{} + if err := writeVarInt(body, 0); err != nil { // packet id + return nil, err + } + if err := writeVarInt(body, 754); err != nil { + return nil, err + } + if err := writeString(body, host); err != nil { + return nil, err + } + if err := binary.Write(body, binary.BigEndian, port); err != nil { + return nil, err + } + if err := writeVarInt(body, 1); err != nil { // next state: status + return nil, err + } + + pkt := &bytes.Buffer{} + if err := writeVarInt(pkt, int32(body.Len())); err != nil { + return nil, err + } + pkt.Write(body.Bytes()) + return pkt.Bytes(), nil +} + +func encodeStatusRequest() ([]byte, error) { + body := &bytes.Buffer{} + if err := writeVarInt(body, 0); err != nil { // packet id + return nil, err + } + pkt := &bytes.Buffer{} + if err := writeVarInt(pkt, int32(body.Len())); err != nil { + return nil, err + } + pkt.Write(body.Bytes()) + return pkt.Bytes(), nil +} + +func decodeStatusResponse(frame []byte) (mcStatusResponse, error) { + // Frame is: packet length varint + packet data. + // We'll parse using a bytes.Reader with ByteReader. + r := bytes.NewReader(frame) + length, err := readVarInt(r) + if err != nil { + return mcStatusResponse{}, err + } + if length <= 0 { + return mcStatusResponse{}, fmt.Errorf("invalid packet length") + } + + // Read exactly length bytes as packet payload + payload := make([]byte, length) + if _, err := io.ReadFull(r, payload); err != nil { + return mcStatusResponse{}, err + } + pr := bytes.NewReader(payload) + packetID, err := readVarInt(pr) + if err != nil { + return mcStatusResponse{}, err + } + if packetID != 0 { + return mcStatusResponse{}, fmt.Errorf("unexpected packet id %d", packetID) + } + + jsonLen, err := readVarInt(pr) + if err != nil { + return mcStatusResponse{}, err + } + if jsonLen < 0 || int(jsonLen) > pr.Len() { + return mcStatusResponse{}, fmt.Errorf("invalid json length") + } + + jsonBytes := make([]byte, jsonLen) + if _, err := io.ReadFull(pr, jsonBytes); err != nil { + return mcStatusResponse{}, err + } + + var resp mcStatusResponse + if err := json.Unmarshal(jsonBytes, &resp); err != nil { + return mcStatusResponse{}, err + } + return resp, nil +} + +func (p *MinecraftPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + host := target.Host + if host == "" { + host = target.Address.Addr().String() + } + + handshake, err := encodeHandshake(host, uint16(target.Address.Port())) + if err != nil { + return nil, err + } + if err := utils.Send(conn, handshake, timeout); err != nil { + return nil, err + } + + statusReq, err := encodeStatusRequest() + if err != nil { + return nil, err + } + respFrame, err := utils.SendRecv(conn, statusReq, timeout) + if err != nil { + return nil, err + } + if len(respFrame) == 0 { + return nil, nil + } + + resp, err := decodeStatusResponse(respFrame) + if err != nil { + return nil, nil + } + + payload := plugins.ServiceMinecraftJava{ + VersionName: resp.Version.Name, + ProtocolVersion: resp.Version.Protocol, + PlayersOnline: resp.Players.Online, + PlayersMax: resp.Players.Max, + Description: resp.Description, + Favicon: resp.Favicon, + EnforcesSecure: resp.SecureChat, + } + + return plugins.CreateServiceFrom(target, payload, false, resp.Version.Name, plugins.TCP), nil +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/minecraft/java/minecraft_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/minecraft/java/minecraft_test.go new file mode 100644 index 0000000..def44c4 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/minecraft/java/minecraft_test.go @@ -0,0 +1,168 @@ +package minecraftjava + +import ( + "bytes" + "encoding/json" + "net" + "net/netip" + "testing" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" +) + +type byteReader struct{ *bytes.Reader } + +func (b byteReader) ReadByte() (byte, error) { return b.Reader.ReadByte() } + +func mustEncodeHandshake(t *testing.T, host string, port uint16) []byte { + t.Helper() + b, err := encodeHandshake(host, port) + if err != nil { + t.Fatalf("encodeHandshake: %v", err) + } + return b +} + +func mustEncodeStatusReq(t *testing.T) []byte { + t.Helper() + b, err := encodeStatusRequest() + if err != nil { + t.Fatalf("encodeStatusRequest: %v", err) + } + return b +} + +func mustFrameStatusJSON(t *testing.T, status any) []byte { + t.Helper() + j, err := json.Marshal(status) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + + // Packet payload: packetID=0 + jsonString + payload := &bytes.Buffer{} + if err := writeVarInt(payload, 0); err != nil { + t.Fatalf("writeVarInt(packetID): %v", err) + } + if err := writeVarInt(payload, int32(len(j))); err != nil { + t.Fatalf("writeVarInt(jsonLen): %v", err) + } + payload.Write(j) + + // Frame: length varint + payload + frame := &bytes.Buffer{} + if err := writeVarInt(frame, int32(payload.Len())); err != nil { + t.Fatalf("writeVarInt(frameLen): %v", err) + } + frame.Write(payload.Bytes()) + return frame.Bytes() +} + +func TestVarIntRoundTrip(t *testing.T) { + vals := []int32{0, 1, 2, 127, 128, 255, 2097151} + for _, v := range vals { + buf := &bytes.Buffer{} + if err := writeVarInt(buf, v); err != nil { + t.Fatalf("writeVarInt(%d): %v", v, err) + } + got, err := readVarInt(byteReader{bytes.NewReader(buf.Bytes())}) + if err != nil { + t.Fatalf("readVarInt(%d): %v", v, err) + } + if got != v { + t.Fatalf("varint mismatch: want %d got %d", v, got) + } + } +} + +func TestMinecraftJavaRunDetectsServer(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + // lightweight fake MC status server on the other end of the pipe + go func() { + defer server.Close() + buf := make([]byte, 4096) + // read handshake + _, _ = server.Read(buf) + // read status request + _, _ = server.Read(buf) + + status := map[string]any{ + "version": map[string]any{"name": "1.20.4", "protocol": 765}, + "players": map[string]any{"max": 20, "online": 3}, + "description": map[string]any{"text": "hello"}, + "favicon": "data:image/png;base64,AAAA", + "enforcesSecureChat": true, + } + frame := mustFrameStatusJSON(t, status) + _, _ = server.Write(frame) + }() + + p := &MinecraftPlugin{} + target := plugins.Target{ + Host: "example.org", + Address: netip.MustParseAddrPort("127.0.0.1:25565"), + } + + service, err := p.Run(client, 2*time.Second, target) + if err != nil { + t.Fatalf("Run returned error: %v", err) + } + if service == nil { + t.Fatalf("expected service, got nil") + } + if service.Protocol != plugins.ProtoMinecraftJava { + t.Fatalf("expected protocol %q got %q", plugins.ProtoMinecraftJava, service.Protocol) + } + if service.Port != 25565 { + t.Fatalf("expected port 25565 got %d", service.Port) + } + + meta := service.Metadata().(plugins.ServiceMinecraftJava) + if meta.VersionName != "1.20.4" { + t.Fatalf("expected versionName 1.20.4 got %q", meta.VersionName) + } + if meta.PlayersOnline != 3 || meta.PlayersMax != 20 { + t.Fatalf("expected players 3/20 got %d/%d", meta.PlayersOnline, meta.PlayersMax) + } +} + +func TestMinecraftJavaRunReturnsNilOnNonMC(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + go func() { + defer server.Close() + // read whatever client writes and then respond with junk + buf := make([]byte, 4096) + _, _ = server.Read(buf) + _, _ = server.Read(buf) + _, _ = server.Write([]byte("not minecraft")) + }() + + p := &MinecraftPlugin{} + target := plugins.Target{Address: netip.MustParseAddrPort("127.0.0.1:25565")} + + svc, err := p.Run(client, 2*time.Second, target) + if err != nil { + t.Fatalf("Run returned error: %v", err) + } + if svc != nil { + t.Fatalf("expected nil service for non-mc response") + } +} + +func TestEncodersBuildPackets(t *testing.T) { + h := mustEncodeHandshake(t, "localhost", 25565) + if len(h) == 0 { + t.Fatalf("handshake packet empty") + } + s := mustEncodeStatusReq(t) + if len(s) == 0 { + t.Fatalf("status request packet empty") + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/modbus/modbus.go b/rigour/pkg/crawler/fingerprint/plugins/services/modbus/modbus.go new file mode 100644 index 0000000..474c19c --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/modbus/modbus.go @@ -0,0 +1,113 @@ +package modbus + +import ( + "bytes" + "crypto/rand" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const ( + ModbusHeaderLength = 7 + ModbusDiscreteInputCode = 0x2 + ModbusErrorAddend = 0x80 +) + +type MODBUSPlugin struct{} + +func init() { + plugins.RegisterPlugin(&MODBUSPlugin{}) +} + +const MODBUS = "modbus" + +func (p *MODBUSPlugin) PortPriority(port uint16) bool { + return port == 502 +} + +// Run +/* + modbus is a communications standard for connecting industrial devices. + modbus can be carried over a number of frame formats; this program identifies + modbus over TCP. + + modbus supports diagnostic functions that could be used for fingerprinting, + however, not all implementations will support the use of these functions. + Therefore, this program utilizes a read primitive and validates both the success + response and the error response conditions. + + modbus supports reading and writing to specified memory addresses using a number + of different primitives. This program utilizes the "Read Discrete Input" primitive, + which requests the value of a read-only boolean. This is the least likely primitive to + be disruptive. + + Additionally, all modbus messages begin with a 7-byte header. The first two bytes are a + client-controlled transaction ID. This program generates a random transaction ID and validates + that the server echos the correct response. + + Initial testing done with `docker run -it -p 502:5020 oitc/modbus-server:latest` + The default TCP port is 502, but this is unofficial. +*/ +func (p *MODBUSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + transactionID := make([]byte, 2) + _, err := rand.Read(transactionID) + if err != nil { + return nil, &utils.RandomizeError{Message: "Transaction ID"} + } + + // Read Discrete Input request + requestBytes := []byte{ + // transaction ID bytes were generated above + // protocol ID (0) + 0x00, 0x00, + // following byte length + 0x00, 0x06, + // remote slave (variable, but fixed to 1 here) + 0x01, + // function code + 0x02, + // starting address of 0x0000 + 0x00, 0x00, + // read one bit. this will cause a successful request to return 1 byte, with the + // 7 high bits set to zero and the low bit set to the response value + 0x00, 0x01, + } + + requestBytes = append(transactionID, requestBytes...) + + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + // transaction ID was echoed correctly + if bytes.Equal(response[:2], transactionID) { + // successful request, validate contents + if response[ModbusHeaderLength] == ModbusDiscreteInputCode { + if response[ModbusHeaderLength+1] == 1 && (response[ModbusHeaderLength+2]>>1) == 0x00 { + return plugins.CreateServiceFrom(target, plugins.ServiceModbus{}, false, "", plugins.TCP), nil + } + } else if response[ModbusHeaderLength] == ModbusDiscreteInputCode+ModbusErrorAddend { + return plugins.CreateServiceFrom(target, plugins.ServiceModbus{}, false, "", plugins.TCP), nil + } + } + return nil, nil +} + +func (p *MODBUSPlugin) Name() string { + return MODBUS +} + +func (p *MODBUSPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *MODBUSPlugin) Priority() int { + return 400 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/modbus/modbus_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/modbus/modbus_test.go new file mode 100644 index 0000000..bd79eb5 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/modbus/modbus_test.go @@ -0,0 +1,38 @@ +package modbus + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestModbus(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "modbus", + Port: 5020, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "oitc/modbus-server", + }, + }, + } + + p := &MODBUSPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt3/mqtt3.go b/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt3/mqtt3.go new file mode 100644 index 0000000..9469e6a --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt3/mqtt3.go @@ -0,0 +1,126 @@ +package mqtt3 + +import ( + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type MQTT3Plugin struct{} +type TLSPlugin struct{} + +const MQTT = "mqtt3" +const MQTTTLS = "mqtt3tls" + +func init() { + plugins.RegisterPlugin(&MQTT3Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func testConnectRequest(conn net.Conn, requestBytes []byte, timeout time.Duration) (bool, error) { + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + if response[0] == 0x20 { + // MQTT server + return true, nil + } + return true, &utils.InvalidResponseError{Service: MQTT} +} + +func (p *MQTT3Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return Run(conn, timeout, false, target) +} + +func (p *MQTT3Plugin) PortPriority(i uint16) bool { + return i == 1883 +} + +func (p *MQTT3Plugin) Name() string { + return MQTT +} + +func (p *MQTT3Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return Run(conn, timeout, true, target) +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 8883 +} + +func (p *TLSPlugin) Name() string { + return MQTTTLS +} + +func (p *MQTT3Plugin) Priority() int { + return 500 +} + +func (p *TLSPlugin) Priority() int { + return 501 +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +// Run +/* + MQTT is a publish-subscribe protocol designed to be used as + a lightweight messaging protocol. An MQTT connection begins with + a CONNECT request and a CONNACK response. A well-behaved MQTT server + will simply close the connection if an invalid request is sent. Connect + packets are formatted slightly differently between v3 and v5, so two requests + are sent. + + CONNECT requests are composed of a fixed header that indicates the message type and + length, and then a variable length header that specifies the connection details, + including the protocol version. The v5 header also includes a properties section, while the + v3 header does not. + + The CONNACK response will begin with a 0x20 byte that indicates the message type. The + presence/absence of this byte is used to determine if MQTT is present. +*/ + +func Run(conn net.Conn, timeout time.Duration, tls bool, target plugins.Target) (*plugins.Service, error) { + // version 3.1.x connect command + mqttConnect3 := []byte{ + // message type 1 + 4 bits reserved + 0x10, + // message length of 17 (the number of following bytes) + 0x11, + // protocol name length (4) + 0x00, 0x04, + // protocol name (MQTT) + 0x4d, 0x51, 0x54, 0x54, + // protocol version (3) + 0x03, + // flags (all unset except for Clean Session) + 0x02, + // keep alive + 0x00, 0x3c, + // client ID length of 5 + 0x00, 0x05, + // client ID AAAA + 0x41, 0x41, 0x41, 0x41, 0x41, + } + + check, err := testConnectRequest(conn, mqttConnect3, timeout) + if check && err == nil { + return plugins.CreateServiceFrom(target, plugins.ServiceMQTT{}, tls, "3.1.x", plugins.TCP), nil + } else if check && err != nil { + return nil, nil + } + return nil, err +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt3/mqtt3_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt3/mqtt3_test.go new file mode 100644 index 0000000..991620b --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt3/mqtt3_test.go @@ -0,0 +1,38 @@ +package mqtt3 + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestMqtt3(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "mqtt", + Port: 1883, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "efrecon/mosquitto", + }, + }, + } + + p := &MQTT3Plugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt5/mqtt5.go b/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt5/mqtt5.go new file mode 100644 index 0000000..0357b8c --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt5/mqtt5.go @@ -0,0 +1,128 @@ +package mqtt5 + +import ( + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type MQTT5Plugin struct{} +type TLSPlugin struct{} + +const MQTT = "mqtt5" +const MQTTTLS = "mqtt5tls" + +func init() { + plugins.RegisterPlugin(&MQTT5Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func testConnectRequest(conn net.Conn, requestBytes []byte, timeout time.Duration) (bool, error) { + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + if response[0] == 0x20 { + // MQTT server + return true, nil + } + return true, &utils.InvalidResponseError{Service: MQTT} +} + +func (p *MQTT5Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return Run(conn, timeout, false, target) +} + +func (p *MQTT5Plugin) PortPriority(i uint16) bool { + return i == 1883 +} + +func (p *MQTT5Plugin) Priority() int { + return 505 +} + +func (p *TLSPlugin) Priority() int { + return 506 +} + +func (p *MQTT5Plugin) Name() string { + return MQTT +} + +func (p *MQTT5Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return Run(conn, timeout, true, target) +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 8883 +} + +func (p *TLSPlugin) Name() string { + return MQTTTLS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +// Run +/* + MQTT is a publish-subscribe protocol designed to be used as + a lightweight messaging protocol. An MQTT connection begins with + a CONNECT request and a CONNACK response. A well-behaved MQTT server + will simply close the connection if an invalid request is sent. Connect + packets are formatted slightly differently between v3 and v5, so two requests + are sent. + + CONNECT requests are composed of a fixed header that indicates the message type and + length, and then a variable length header that specifies the connection details, + including the protocol version. The v5 header also includes a properties section, while the + v3 header does not. + + The CONNACK response will begin with a 0x20 byte that indicates the message type. The + presence/absence of this byte is used to determine if MQTT is present. +*/ + +func Run(conn net.Conn, timeout time.Duration, tls bool, target plugins.Target) (*plugins.Service, error) { + // version 3.1.1 connect command + mqttConnect5 := []byte{ + // message type 1 + 4 bits reserved + 0x10, + // message length of 18 (the number of following bytes) + 0x12, + // protocol name length (4) + 0x00, 0x04, + // protocol name (MQTT) + 0x4d, 0x51, 0x54, 0x54, + // protocol version (5) + 0x05, + // flags (all unset except for Clean Session) + 0x02, + // keep alive + 0x00, 0x3c, + // properties length of 0 + 0x00, + // client ID length of 5 + 0x00, 0x05, + // client ID AAAA + 0x41, 0x41, 0x41, 0x41, 0x41, + } + + check, err := testConnectRequest(conn, mqttConnect5, timeout) + if check && err == nil { + return plugins.CreateServiceFrom(target, plugins.ServiceMQTT{}, tls, "5.0", plugins.TCP), nil + } else if check && err != nil { + return nil, nil + } + return nil, err +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt5/mqtt5_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt5/mqtt5_test.go new file mode 100644 index 0000000..a3b642d --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/mqtt/mqtt5/mqtt5_test.go @@ -0,0 +1,38 @@ +package mqtt5 + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestMqtt5(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "mqtt", + Port: 1883, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "efrecon/mosquitto", + }, + }, + } + + p := &MQTT5Plugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/mssql/mssql.go b/rigour/pkg/crawler/fingerprint/plugins/services/mssql/mssql.go new file mode 100644 index 0000000..cd98735 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/mssql/mssql.go @@ -0,0 +1,316 @@ +package mssql + +import ( + "fmt" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +// Potential values for PLOptionToken +const ( + VERSION int = 0 + ENCRYPTION int = 1 + INSTOPT int = 2 + THREADID int = 3 + MARS int = 4 + TRACEID int = 5 + FEDAUTHREQUIRED int = 6 + NONCEOPT int = 7 + TERMINATOR byte = 0xFF +) + +type OptionToken struct { + PLOptionToken uint32 + PLOffset uint32 + PLOptionLength uint32 + PLOptionData []byte // the raw data associated with the option +} + +type MSSQLPlugin struct{} + +type Data struct { + Version string +} + +const MSSQL = "mssql" + +func init() { + plugins.RegisterPlugin(&MSSQLPlugin{}) +} + +func (p *MSSQLPlugin) PortPriority(port uint16) bool { + return port == 1433 +} + +func DetectMSSQL(conn net.Conn, timeout time.Duration) (Data, bool, error) { + // Below is a TDS prelogin packet sent by the client to begin the + // initial handshake with the server + preLoginPacket := []byte{ + + // Pre-Login Request Header + 0x12, // Type + 0x01, // Status + 0x00, 0x58, // Length + 0x00, 0x00, // SPID + 0x01, // PacketID + 0x00, // Window + + // We configure the following options within the pre-login request body: + // + // VERSION: 11 09 00 01 00 00 + // ENCRYPTION: 00 + // INSTOPT: 00 + // THREADID: 00 00 00 00 + // MARS: 00 + // TRACEID: f9 b8 cb 5c 94 6b 89 1f + // d9 aa 3c 13 4b d0 7b 88 + // 03 5c 32 21 24 a2 81 86 + // 37 cf 62 39 4a 46 2c c6 + // 00 00 00 00 + + // Pre-Login Request Payload + 0x00, // PLOptionToken (VERSION) + 0x00, 0x1F, // PLOffset + 0x00, 0x06, // PLOptionLength + + 0x01, // PLOptionToken (ENCRYPTION) + 0x00, 0x25, // PLOffset + 0x00, 0x01, // PLOptionLength + + 0x02, // PLOptionToken (INSTOPT) + 0x00, 0x26, // PLOffset + 0x00, 0x01, // PLOptionLength + + 0x03, // PLOptionToken (THREADID) + 0x00, 0x27, // PLOffset + 0x00, 0x04, // PLOptionLength + + 0x04, // PLOptionToken (MARS) + 0x00, 0x2B, // PLOffset + 0x00, 0x01, // PLOptionLength + + 0x05, // PLOptionToken (TRACEID) + 0x00, 0x2C, // PLOffset + 0x00, 0x24, // PLOptionLength + + 0xFF, // TERMINATOR + + // PLOptionData + 0x11, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF9, 0xB8, 0xCB, + 0x5C, 0x94, 0x6B, 0x89, 0x1F, 0xD9, 0xAA, 0x3C, + 0x13, 0x4B, 0xD0, 0x7B, 0x88, 0x03, 0x5C, 0x32, + 0x21, 0x24, 0xA2, 0x81, 0x86, 0x37, 0xCF, 0x62, + 0x39, 0x4A, 0x46, 0x2C, 0xC6, 0x00, 0x00, 0x00, + 0x00, + } + + response, err := utils.SendRecv(conn, preLoginPacket, timeout) + if err != nil { + return Data{}, false, err + } + if len(response) == 0 { + return Data{}, true, &utils.ServerNotEnable{} + } + + /* + Below is an example pre-login response (tabular response) packet + returned by the client to the server: + + Pre-Login Response (Tabular Response) Header: + + Type: 0x04 + Status: 0x01 + Length: 0x00 0x30 + SPID: 0x00 0x00 + PacketId: 0x01 + Window: 0x00 + + Pre-Login Response Body: + + PLOptionToken: 0x00 (VERSION) + PLOffset: 0x00 0x1F + PLOptionLength: 0x00 0x06 + + PLOptionToken: 0x01 (ENCRYPTION) + PLOffset: 0x00 0x25 + PLOptionLength: 0x00 0x01 + + PLOptionToken: 0x02 (INSTOPT) + PLOffset: 0x00 0x26 + PLOptionLength: 0x00 0x01 + + PLOptionToken: 0x03 (THREADID) + PLOffset: 0x00 0x27 + PLOptionLength: 0x00 0x00 + + PLOptionToken: 0x04 (MARS) + PLOffset: 0x00 0x27 + PLOptionLength: 0x00 0x01 + + PLOptionToken: 0x05 (TRACEID) + PLOffset: 0x00 0x28 + PLOptionLength: 0x00 0x00 + + PLOptionToken: 0xFF + + PLOptionData: 0f 00 07 d0 00 00 00 00 00 + + VERSION: 0f 00 07 d0 00 00 + ENCRYPTION: 00 + INSTOPT 00 + MARS: 00 + */ + + // The TDS header is eight bytes so any response less than this can be safely classified + // as invalid (i.e. not MSSQL/TDS) + if len(response) < 8 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "response is too short to be a valid TDS packet header", + } + } + + if response[0] != 0x04 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "type should be set to tabular result for a valid TDS packet", + } + } + + if response[1] != 0x01 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "expect a status of one (end of message) for tabular result packet", + } + } + + packetLength := int(uint32(response[3]) | uint32(response[2])<<8) + if len(response) != packetLength { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "packet length does not match length read", + } + } + + if response[4] != 0x00 || response[5] != 0x00 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "value for SPID should always be zero", + } + } + + if response[6] != 0x01 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "value for packet id should always be one", + } + } + + if response[7] != 0x00 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "value for window should always be zero", + } + } + + // The body of the pre-login response message is a list of PL_OPTION tokens + // that index into the PLOptionData segment and the list is + // terminated by a PLOptionToken with TERMINATOR (0xFF) as the value. + + position := 8 // set to the position to just after the TDS packet header + + var optionTokens []OptionToken + for response[position] != TERMINATOR && position < len(response) { + plOptionToken := uint32(response[position+0]) + plOffset := uint32(response[position+2]) | uint32(response[position+1])<<8 + plOptionLength := uint32(response[position+4]) | uint32(response[position+3])<<8 + + plOptionData := []byte{} + if plOptionLength != 0 { + if plOffset+plOptionLength < uint32(len(response)) { + plOptionData = response[plOffset+8 : plOffset+8+plOptionLength] + } else { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "server returned an invalid PLOffset or PLOptionLength"} + } + } + + position += 5 + optionTokenStruct := OptionToken{ + PLOptionToken: plOptionToken, + PLOffset: plOffset, + PLOptionLength: plOptionLength, + PLOptionData: plOptionData, + } + + optionTokens = append(optionTokens, optionTokenStruct) + } + + if response[position] != 0xFF { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "list of option tokens should be terminated by 0xff", + } + } + + if len(optionTokens) < 1 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "there should be at least one option token since VERSION is required", + } + } + + if optionTokens[0].PLOptionToken != 0x00 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "TDS requires VERSION to be the first PLOptionToken value", + } + } + + if optionTokens[0].PLOptionLength != 0x06 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "version field should be fixed bytes", + } + } + + MajorVersion := optionTokens[0].PLOptionData[0] + MinorVersion := optionTokens[0].PLOptionData[1] + BuildNumber := uint32( + (uint32(optionTokens[0].PLOptionData[2]) * 256) + uint32( + optionTokens[0].PLOptionData[3], + ), + ) + + version := fmt.Sprintf("%d.%d.%d\n", MajorVersion, MinorVersion, BuildNumber) + + return Data{Version: version}, true, nil +} + +func (p *MSSQLPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + data, check, err := DetectMSSQL(conn, timeout) + if check && err != nil { + return nil, nil + } else if !check && err != nil { + return nil, err + } + + return plugins.CreateServiceFrom(target, plugins.ServiceMSSQL{}, false, data.Version, plugins.TCP), nil +} + +func (p *MSSQLPlugin) Name() string { + return MSSQL +} + +func (p *MSSQLPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *MSSQLPlugin) Priority() int { + return 143 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/mssql/mssql_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/mssql/mssql_test.go new file mode 100644 index 0000000..b04045f --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/mssql/mssql_test.go @@ -0,0 +1,43 @@ +package mssql + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestMSSQL(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "mssql", + Port: 1433, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "mcr.microsoft.com/mssql/server", + Tag: "2019-latest", + Env: []string{ + "ACCEPT_EULA=Y", + "SA_PASSWORD=yourStrong(!)Password", + }, + }, + }, + } + + p := &MSSQLPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/mysql/mysql.go b/rigour/pkg/crawler/fingerprint/plugins/services/mysql/mysql.go new file mode 100644 index 0000000..a305d0e --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/mysql/mysql.go @@ -0,0 +1,277 @@ +package mysql + +import ( + "fmt" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +/* +When we perform fingerprinting of the MySQL service, we can expect to get one +of two packets back from the server on the initial connection. The first would +be an initial handshake packet indicating we can authenticate to the server. + +The second potential response would be an error message returned by the server +telling us why we can't authenticate. For example, the server may respond with +an error message stating the client IP is not allowed to authenticate to the +server. + + Example MySQL Initial Handshake Packet: + Length: 4a 00 00 00 + Version: 0a + Server Version: 38 2e 30 2e 32 38 00 (null terminated string "8.0.28") + Connection Id: 0b 00 00 00 + Auth-Plugin-Data-Part-1: 15 05 6c 51 28 32 48 15 + Filler: 00 + Capability Flags: ff ff + Character Set: ff + Status Flags: 02 00 + Capability Flags: ff df + Length of Auth Plugin Data: 15 + Reserved (all 00): 00 00 00 00 00 00 00 00 00 00 + Auth-Plugin-Data-Part-2 (len 13 base 10): 26 68 15 1e 2e 7f 69 38 52 6b 6c 5c 00 + Auth Plugin Name: null terminated string "caching_sha2_password" + + Example MySQL Error Packet on Initial Connection: + Packet Length: 45 00 00 00 + Header: ff + Error Code: 6a 04 + Human Readable Error Message: Host '50.82.91.234' is not allowed to connect to this MySQL server +*/ + +type MYSQLPlugin struct{} + +const ( + // protocolVersion = 10 + // maxPacketLength = 1<<24 - 1 + MYSQL = "MySQL" +) + +func init() { + plugins.RegisterPlugin(&MYSQLPlugin{}) +} + +// Run checks if the identified service is a MySQL (or MariaDB) server using +// two methods. Upon the connection of a client to a MySQL server it can return +// one of two responses. Either the server returns an initial handshake packet +// or an error message packet. +func (p *MYSQLPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + response, err := utils.Recv(conn, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + mysqlVersionStr, err := CheckInitialHandshakePacket(response) + if err == nil { + payload := plugins.ServiceMySQL{ + PacketType: "handshake", + ErrorMessage: "", + ErrorCode: 0, + } + return plugins.CreateServiceFrom(target, payload, false, mysqlVersionStr, plugins.TCP), nil + } + + errorStr, errorCode, err := CheckErrorMessagePacket(response) + if err == nil { + payload := plugins.ServiceMySQL{ + PacketType: "error", + ErrorMessage: errorStr, + ErrorCode: errorCode, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + return nil, nil +} + +func (p *MYSQLPlugin) PortPriority(port uint16) bool { + return port == 3306 +} + +func (p *MYSQLPlugin) Name() string { + return MYSQL +} + +func (p *MYSQLPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *MYSQLPlugin) Priority() int { + return 133 +} + +// CheckErrorMessagePacket checks the response packet error message +func CheckErrorMessagePacket(response []byte) (string, int, error) { + // My brief research suggests that its not possible to get a compliant + // error message packet that is less than eight bytes + if len(response) < 8 { + return "", 0, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet is too small for an error message packet", + } + } + + packetLength := int( + uint32( + response[0], + ) | uint32( + response[1], + )<<8 | uint32( + response[2], + )<<16 | uint32( + response[3], + )<<24, + ) + actualResponseLength := len(response) - 4 + + if packetLength != actualResponseLength { + return "", 0, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet length does not match length of the response from the server", + } + } + + header := int(response[4]) + if header != 0xff { + return "", 0, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet has an invalid header for an error message packet", + } + } + + errorCode := int(uint32(response[5]) | uint32(response[6])<<8) + if errorCode < 1000 || errorCode > 2000 { + return "", errorCode, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet has an invalid error code", + } + } + + errorStr, err := readEOFTerminatedASCIIString(response, 7) + if err != nil { + return "", errorCode, &utils.InvalidResponseErrorInfo{Service: MYSQL, Info: err.Error()} + } + + return errorStr, errorCode, nil +} + +// CheckInitialHandshakePacket checks if the response received from the server +// matches the expected response for the MySQL service +func CheckInitialHandshakePacket(response []byte) (string, error) { + // My brief research suggests that its not possible to get a compliant + // initial handshake packet that is less than roughly 35 bytes + if len(response) < 35 { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet length is too small for an initial handshake packet", + } + } + + packetLength := int( + uint32( + response[0], + ) | uint32( + response[1], + )<<8 | uint32( + response[2], + )<<16 | uint32( + response[3], + )<<24, + ) + version := int(response[4]) + + if packetLength < 25 || packetLength > 4096 { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet length doesn't make sense for the MySQL handshake packet", + } + } + + if version != 10 { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet has an invalid version", + } + } + + mysqlVersionStr, position, err := readNullTerminatedASCIIString(response, 5) + if err != nil { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "unable to read null-terminated ASCII version string, err: " + err.Error(), + } + } + + // If we skip the connection id and auth-plugin-data-part-1 fields the spec says + // there is a filler byte that should always be zero at this position + fillerPos := position + 13 + if position >= len(response) { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "buffer is too small to be a valid initial handshake packet", + } + } + + // According to the specification this should always be zero since it is a filler byte + if response[fillerPos] != 0x00 { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: fmt.Sprintf( + "expected filler byte at ths position to be zero got: %d", + response[fillerPos], + ), + } + } + + return mysqlVersionStr, nil +} + +// readNullTerminatedASCIIString is responsible for reading a null terminated +// ASCII string from a buffer and returns it as a string type +func readNullTerminatedASCIIString(buffer []byte, startPosition int) (string, int, error) { + characters := []byte{} + success := false + endPosition := 0 + + for position := startPosition; position < len(buffer); position++ { + if buffer[position] >= 0x20 && buffer[position] <= 0x7E { + characters = append(characters, buffer[position]) + } else if buffer[position] == 0x00 { + success = true + endPosition = position + break + } else { + return "", 0, &utils.InvalidResponseErrorInfo{Service: MYSQL, Info: "encountered invalid ASCII character"} + } + } + + if !success { + return "", 0, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "hit the end of the buffer without encountering a null terminator", + } + } + + return string(characters), endPosition, nil +} + +// readEOFTerminatedASCIIString is responsible for reading an ASCII string +// that is terminated by the end of the message +func readEOFTerminatedASCIIString(buffer []byte, startPosition int) (string, error) { + characters := []byte{} + + for position := startPosition; position < len(buffer); position++ { + if buffer[position] >= 0x20 && buffer[position] <= 0x7E { + characters = append(characters, buffer[position]) + } else { + return "", &utils.InvalidResponseErrorInfo{Service: MYSQL, Info: "encountered invalid ASCII character"} + } + } + + return string(characters), nil +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/mysql/mysql_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/mysql/mysql_test.go new file mode 100644 index 0000000..630a5a8 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/mysql/mysql_test.go @@ -0,0 +1,46 @@ +package mysql + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestMySQL(t *testing.T) { + // Flaky in CI: MySQL container readiness varies and can exceed our fixed + // sleeps/retry window. + t.Skip("skipping flaky docker integration test") + + testcases := []test.Testcase{ + { + Description: "mysql", + Port: 3306, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "mysql", + Tag: "5.7.39", + Env: []string{ + "MYSQL_ROOT_PASSWORD=my-secret-pw", + }, + }, + }, + } + + p := &MYSQLPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/netbios/netbiosns.go b/rigour/pkg/crawler/fingerprint/plugins/services/netbios/netbiosns.go new file mode 100644 index 0000000..33b31cb --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/netbios/netbiosns.go @@ -0,0 +1,72 @@ +package netbios + +import ( + "crypto/rand" + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const NETBIOS = "netbios-ns" + +type Plugin struct{} + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + transactionID := make([]byte, 2) + _, err := rand.Read(transactionID) + if err != nil { + return nil, &utils.RandomizeError{Message: "Transaction ID"} + } + InitialConnectionPackage := append(transactionID, []byte{ //nolint:gocritic + // Transaction ID + 0x00, 0x10, // Flag: Broadcast + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Queries + 0x20, 0x43, 0x4b, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, + 0x00, 0x21, + 0x00, 0x01, + }...) + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + stringBegin := strings.Index(string(response), "\x00\x00\x00\x00\x00") + 7 + stringEnd := strings.Index(string(response), "\x20\x20\x20") + if stringBegin == -1 || stringEnd == -1 || stringEnd < stringBegin || + stringBegin >= len(response) || stringEnd >= len(response) { + return nil, nil + } + payload := plugins.ServiceNetbios{ + NetBIOSName: string(response[stringBegin:stringEnd]), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 137 +} + +func (p *Plugin) Name() string { + return NETBIOS +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *Plugin) Priority() int { + return 700 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/netbios/netbiosns_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/netbios/netbiosns_test.go new file mode 100644 index 0000000..d2b7851 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/netbios/netbiosns_test.go @@ -0,0 +1,40 @@ +package netbios + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestNetBIOS(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "netbios-ns", + Port: 137, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "dperson/samba", + Cmd: []string{"-n"}, + Privileged: true, + }, + }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ntp/ntp.go b/rigour/pkg/crawler/fingerprint/plugins/services/ntp/ntp.go new file mode 100644 index 0000000..31bddf7 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ntp/ntp.go @@ -0,0 +1,67 @@ +package ntp + +import ( + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const NTP = "ntp" + +type Plugin struct{} + +var ModeServer uint8 = 4 + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + // reference: https://datatracker.ietf.org/doc/html/rfc5905#section-7.3 + InitialConnectionPackage := []byte{ + 0xe3, 0x00, 0x0a, 0xf8, // LI/VN/Mode | Stratum | Poll | Precision + 0x00, 0x00, 0x00, 0x00, // Root Delay + 0x00, 0x00, 0x00, 0x00, // Root Dispersion + 0x00, 0x00, 0x00, 0x00, // Reference Identifier + 0x00, 0x00, 0x00, 0x00, // Reference Timestamp + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // Origin Timestamp + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // Receive Timestamp + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // Transmit Timestamp + 0x00, 0x00, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + // check if response is valid NTP packet + if response[0]&0x07 == ModeServer && len(response) == len(InitialConnectionPackage) { + return plugins.CreateServiceFrom(target, plugins.ServiceNTP{}, false, "", plugins.UDP), nil + } + return nil, nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 123 +} + +func (p *Plugin) Name() string { + return NTP +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *Plugin) Priority() int { + return 800 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ntp/ntp_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/ntp/ntp_test.go new file mode 100644 index 0000000..f3e20d9 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ntp/ntp_test.go @@ -0,0 +1,37 @@ +package ntp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestNTP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ntp", + Port: 123, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "cturra/ntp", + }, + }, + } + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/openvpn/openvpn.go b/rigour/pkg/crawler/fingerprint/plugins/services/openvpn/openvpn.go new file mode 100644 index 0000000..68859df --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/openvpn/openvpn.go @@ -0,0 +1,85 @@ +package openvpn + +import ( + "crypto/rand" + "net" + "reflect" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const OPENVPN = "OpenVPN" + +type Plugin struct{} + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + /** + * https://build.openvpn.net/doxygen/ssl__pkt_8h_source.html + * https://openvpn.net/community-resources/openvpn-protocol/ + * + * Send CLIENT_RESET control message, expect back valid SERVER_RESET message from server + * Checks if SERVER_RESET opcode is received, along with whether remote session ID is contained in response + * NOTE: Does not work if tls-auth is enabled in OpenVPN config (drops connection due to HMAC error) + */ + + var POpcodeShift uint8 = 3 + var PControlHardResetClientV2 uint8 = 7 + var PControlHardResetServerV2 uint8 = 8 + var SessionIDLength = 8 + + InitialConnectionPackage := []byte{ + PControlHardResetClientV2 << POpcodeShift, // opcode/key_id + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // Session ID (64-bit), + 0x0, // Message Packet-ID Array Length + 0x0, 0x0, 0x0, 0x0, // Message Packet-ID + } + _, err := rand.Read( + InitialConnectionPackage[1 : 1+SessionIDLength], + ) // generate random session ID + if err != nil { + return nil, &utils.RandomizeError{Message: "session ID"} + } + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + // check if response is valid OpenVPN packet + if (response[0] >> POpcodeShift) == PControlHardResetServerV2 { + for i := 0; i < len(response)-SessionIDLength; i++ { + if reflect.DeepEqual( + response[i:i+SessionIDLength], + InitialConnectionPackage[1:1+SessionIDLength], + ) { + return plugins.CreateServiceFrom(target, plugins.ServiceOpenVPN{}, false, "", plugins.UDP), nil + } + } + } + return nil, nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 1194 +} + +func (p *Plugin) Name() string { + return OPENVPN +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *Plugin) Priority() int { + return 708 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/openvpn/openvpn_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/openvpn/openvpn_test.go new file mode 100644 index 0000000..7a35e2f --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/openvpn/openvpn_test.go @@ -0,0 +1,38 @@ +package openvpn + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/test" +) + +func TestOpenVPN(t *testing.T) { + // the Privileged container does not run on Github actions -- but this test passes locally + testcases := []test.Testcase{ + // { + // Description: "openvpn", + // Port: 1194, + // Protocol: plugins.UDP, + // Expected: func(res *plugins.PluginResults) bool { + // return res != nil + // }, + // RunConfig: dockertest.RunOptions{ + // Repository: "jpetazzo/dockvpn", + // Privileged: true, + // }, + // }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/oracledb/oracle.go b/rigour/pkg/crawler/fingerprint/plugins/services/oracledb/oracle.go new file mode 100644 index 0000000..1136ca8 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/oracledb/oracle.go @@ -0,0 +1,253 @@ +package oracledb + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/big" + "net" + "regexp" + "strconv" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type ORACLEPlugin struct{} + +const ORACLE = "oracle" + +func init() { + plugins.RegisterPlugin(&ORACLEPlugin{}) +} + +/* +Transparent Network Substrate Protocol + + Packet Length: 270 + Packet Checksum: 0x0000 + Packet Type: Connect (1) + Reserved Byte: 00 + Header Checksum: 0x0000 + Connect + Version: 318 + Version (Compatible): 300 + Service Options: 0x0c41, Header Checksum, Full Duplex + Session Data Unit Size: 8192 + Maximum Transmission Data Unit Size: 65535 + NT Protocol Characteristics: 0x7f08, Confirmed release, TDU based IO, Spawner running, Data test, + Callback IO supported, ASync IO Supported, Packet oriented IO, + Generate SIGURG signal + Line Turnaround Value: 0 + Value of 1 in Hardware: 0100 + Length of Connect Data: 196 + Offset to Connect Data: 74 + Maximum Receivable Connect Data: 5120 + Connect Flags 0: 0x41, NA services wanted + ...0 .... = NA services required: False + .... 0... = NA services linked in: False + .... .0.. = NA services enabled: False + .... ..0. = Interchange is involved: False + .... ...1 = NA services wanted: True + Connect Flags 1: 0x41, NA services wanted + ...0 .... = NA services required: False + .... 0... = NA services linked in: False + .... .0.. = NA services enabled: False + .... ..0. = Interchange is involved: False + .... ...1 = NA services wanted: True + Trace Cross Facility Item 1: 0xd8870000 + Trace Cross Facility Item 2: 0x00000000 + Trace Unique Connection ID: 0x0000000000000000 + Connect Data: (DESCRIPTION=(CONNECT_DATA=(SERVICE_NAME=XE)(CID=(PROGRAM=sqlplus) + (HOST=a68e91558f29)(USER=oracle))(CONNECTION_ID=3krKWwBDEZ/gUwQAEaydrQ==)) + (ADDRESS=(PROTOCOL=tcp)(HOST=192.168.1.116)(PORT=1521))) + + + Expected Rejection Response: + Transparent Network Substrate Protocol + Packet Length: 103 + Packet Checksum: 0x0000 + Packet Type: Refuse (4) + Reserved Byte: 00 + Header Checksum: 0x0000 + Refuse + Refuse Reason (User): 0x22 + Refuse Reason (System): 0x00 + Refuse Data Length: 91 + Refuse Data: (DESCRIPTION=(TMP=)(VSNNUM=352321536)(ERR=12514)(ERROR_STACK=(ERROR=(CODE=12514)(EMFI=4)))) + + + Transparent Network Substrate Protocol + Packet Length: 8 + Packet Checksum: 0x0000 + Packet Type: Resend (11) + Reserved Byte: 00 + Header Checksum: 0x0000 + + + TESTED AGAINST: Oracle DB XE 21c; however, I could not find an 11g download link which is required + to build the container for testing. Also please note the heuristic-ness of the response verifications. + The request settings may need to be tinkered with (areas of tinkering have been noted with comments) + + Oracle Database 10.2, 11.x, 12.x, and 18c are available as a media or FTP request + for those customers who own a valid Oracle Database product license for any edition. + To request access to these releases, follow the instructions in Oracle Support Document + 1071023.1 (Requesting Physical Shipment or Download URL for Software Media) from My Oracle Support. + NOTE: for Oracle Database 10.2, you should request 10.2.0.1 even if you want to install a later + patch set. Once you install 10.2.0.1 you can then apply any 10.2 patch set. Similarly, for + 11.1 request 11.1.0.6 which must be applied before installing 11.1.0.7. Patch sets can be + downloaded from the Patches and Updates tab on My Oracle Support. +*/ +func checkForOracle(host string, port string) []byte { + // This string varies widely for several scripts from metasploit and nmap, + // probably needs to be adapted over time as we get feedback on how effective it is + connectData := []byte( + "(DESCRIPTION=(CONNECT_DATA=(SERVICE_NAME=non-abc-existent-ser-vice-123-a-a-bc-asdf)(CID=(PROGRAM=sqlplus)" + + "(HOST=__jdbc__)(USER=)))(ADDRESS=(PROTOCOL=tcp)(HOST=" + host + ")(PORT=" + port + ")))", + ) + tnsHeaderPktLen := [2]byte{} + binary.BigEndian.PutUint16(tnsHeaderPktLen[:], uint16(len(connectData)+58)) + tnsHeaderPktCkSm := [2]byte{0x00, 0x00} // This is left empty + tnsHeaderPktType := [1]byte{0x01} // Connect type + tnsHeaderReservedByte := [1]byte{0x00} + tnsHeaderChecksum := [2]byte{0x00, 0x00} + + connectVersion := [2]byte{0x01, 0x3c} + connectVersionCompat := [2]byte{0x01, 0x2c} + connectServiceOpts := [2]byte{0x00, 0x00} + sessionDUS := [2]byte{0x80, 0x00} // Data Unit Size + maxSessionDUS := [2]byte{0x7F, 0xFF} + ntPrtoChar := [2]byte{0x7F, 0x08} + lineTurnaroundVal := [2]byte{0x00, 0x00} + valOneInHardware := [2]byte{0x00, 0x01} + lenConnectData := [2]byte{} + binary.BigEndian.PutUint16(lenConnectData[:], uint16(len(connectData))) + offsetConnectData := [2]byte{0x00, 0x3a} + MaxDataRecv := [4]byte{0x00, 0x00, 0x04, 0x00} + connectFlags := [2]byte{0x00, 0x00} + traceCrossFacilityItems := [8]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + traceUnqConnID := [8]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0} + magicBytes := [8]byte{ + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + } // These bytes are undocumented, and I believe + // Are dependent on the version of TNS used and some connect and NT flags. + // This may be a focus for reliability and accuracy + combine := [][]byte{ + tnsHeaderPktLen[:], + tnsHeaderPktCkSm[:], + tnsHeaderPktType[:], + tnsHeaderReservedByte[:], + tnsHeaderChecksum[:], + connectVersion[:], + connectVersionCompat[:], + connectServiceOpts[:], + sessionDUS[:], + maxSessionDUS[:], + ntPrtoChar[:], + lineTurnaroundVal[:], + valOneInHardware[:], + lenConnectData[:], + offsetConnectData[:], + MaxDataRecv[:], + connectFlags[:], + traceCrossFacilityItems[:], + traceUnqConnID[:], + magicBytes[:], + connectData, + } + + fullRequest := make([]byte, len(connectData)+58) + index := 0 + for _, s := range combine { + index += copy(fullRequest[index:], s) + } + + return fullRequest +} + +func isOracleDBRunning(response []byte) bool { + beginPattern := []byte{ + 0x28, 0x44, 0x45, 0x53, 0x43, 0x52, 0x49, 0x50, + 0x54, 0x49, 0x4f, 0x4e, 0x3d, 0x28, 0x54, 0x4d, + 0x50, 0x3d, 0x29, 0x28, 0x56, 0x53, 0x4e, 0x4e, + 0x55, 0x4d, 0x3d, + } + + if len(response) < 27 { + return false + } + + responseCode := int(response[4]) + + // This should always be a response code of 4 (rejection), + // however I have included resend and accept response codes as well + if responseCode != 4 && responseCode != 2 && responseCode != 11 { + return false + } + + // When making a request with the function above, every oracle version should return a variation of: + // (DESCRIPTION=(TMP=)(VSNNUM=318767104)(ERR=1189)(ERROR_STACK=(ERROR=(CODE=1189)(EMFI=4)))) + // VSNUM and ERR will change based on the version of oracle used + // Instead, key off (DESCRIPTION=(TMP=)(VSNNUM= to determine if the server is running oracle + return bytes.Index(response, beginPattern) > 0 +} + +func parseInfo(response []byte) map[string]any { + refuseData := response[12:] + code := regexp.MustCompile(`[0-9]+`).FindAllStringSubmatch(string(refuseData), 2) + VSNNum := code[0][0] + ErrCode := code[1][0] + VsNum, _ := strconv.Atoi(VSNNum) + version := big.NewInt(int64(VsNum)).Bytes() + split := strconv.FormatInt(int64(version[1]), 16) + versionStr := fmt.Sprintf("%d.%c.%c.%d.%d", version[0], split[0], split[1], version[2], version[3]) + return map[string]any{"Oracle TNS Listener Version": versionStr, "VSNNUM": VSNNum, "ERROR_CODE": ErrCode} +} + +func (p *ORACLEPlugin) PortPriority(port uint16) bool { + return port == 1521 +} + +func (p *ORACLEPlugin) Name() string { + return ORACLE +} + +func (p *ORACLEPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *ORACLEPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + addr := strings.Split(conn.RemoteAddr().String(), ":") + ip, port := addr[0], addr[1] + request := checkForOracle(ip, port) + + response, err := utils.SendRecv(conn, request, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + if isOracleDBRunning(response) { + oracleInfo := fmt.Sprintf("%s", parseInfo(response)) + payload := plugins.ServiceOracle{ + Info: oracleInfo, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + return nil, nil +} + +func (p *ORACLEPlugin) Priority() int { + return 900 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/oracledb/oracle_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/oracledb/oracle_test.go new file mode 100644 index 0000000..cdc06af --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/oracledb/oracle_test.go @@ -0,0 +1,39 @@ +package oracledb + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestOracleDB(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "oracledb", + Port: 1521, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "babim/oracledatabase", + Tag: "11.2.0.4", + }, + }, + } + + p := &ORACLEPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/pop3/pop3.go b/rigour/pkg/crawler/fingerprint/plugins/services/pop3/pop3.go new file mode 100644 index 0000000..2f58441 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/pop3/pop3.go @@ -0,0 +1,127 @@ +package pop3 + +import ( + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type POP3Plugin struct{} // POP3 +type TLSPlugin struct{} // POP3S + +const POP3 = "pop3" +const POP3S = "pop3s" + +func init() { + plugins.RegisterPlugin(&POP3Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func (p *POP3Plugin) PortPriority(port uint16) bool { + return port == 110 +} + +func DetectPOP3(conn net.Conn, timeout time.Duration, tls bool) (string, bool, error) { + // read initial response from server + initialResponse, err := utils.Recv(conn, timeout) + if err != nil { + return "", false, err + } + if len(initialResponse) == 0 { + return "", true, &utils.ServerNotEnable{} + } + + // send a bogus command and read error response + errResponse, err := utils.SendRecv(conn, []byte("Not a command \r\n"), timeout) + if err != nil { + return "", false, err + } + if len(errResponse) == 0 { + return "", true, &utils.ServerNotEnable{} + } + + isPOP3 := false + if strings.HasPrefix(string(initialResponse), "+OK") && + strings.HasPrefix(string(errResponse), "-ERR") { + isPOP3 = true + } + + if !isPOP3 { + // no ? :( + if tls { + return "", true, &utils.InvalidResponseErrorInfo{ + Service: POP3S, + Info: "did not get expected banner for POP3S", + } + } + return "", true, &utils.InvalidResponseErrorInfo{ + Service: POP3, + Info: "did not get expected banner for POP3", + } + } + + return string(initialResponse[4:]), true, nil +} + +func (p *POP3Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, check, err := DetectPOP3(conn, timeout, false) + + if check && err != nil { // service is not running POP3 + return nil, nil + } else if !check && err != nil { // plugin error + return nil, err + } + + // service is running POP3 + payload := plugins.ServicePOP3{ + Banner: result, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *TLSPlugin) PortPriority(port uint16) bool { + return port == 995 +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, check, err := DetectPOP3(conn, timeout, true) + + if check && err != nil { // service is not running POP3S + return nil, nil + } else if !check && err != nil { // plugin error + return nil, err + } + + // service is running POP3S + payload := plugins.ServicePOP3{ + Banner: result, + } + return plugins.CreateServiceFrom(target, payload, true, "", plugins.TCP), nil +} + +func (p *POP3Plugin) Name() string { + return POP3 +} + +func (p *POP3Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Name() string { + return POP3S +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *POP3Plugin) Priority() int { + return 120 +} + +func (p *TLSPlugin) Priority() int { + return 122 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/pop3/pop3_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/pop3/pop3_test.go new file mode 100644 index 0000000..5b76c21 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/pop3/pop3_test.go @@ -0,0 +1,39 @@ +package pop3 + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestPOP3(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "pop3", + Port: 110, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "instrumentisto/dovecot", + Name: "dovecot-pop3-test", + }, + }, + } + + p := &POP3Plugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/postgresql/postgresql.go b/rigour/pkg/crawler/fingerprint/plugins/services/postgresql/postgresql.go new file mode 100644 index 0000000..2588f52 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/postgresql/postgresql.go @@ -0,0 +1,133 @@ +package postgres + +import ( + "encoding/binary" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type POSTGRESPlugin struct{} + +const POSTGRES = "postgres" + +// https://www.postgresql.org/docs/current/protocol-flow.html +// the following three values are the only three valid responses +// from a server for the first byte +const ErrorResponse byte = 0x45 + +// all of the following messages start with R (0x52) +// AuthenticationOk +// AuthenticationKerberosV5 +// AuthenticationCleartextPassword +// AuthenticationMD5Password +// AuthenticationSCMCredential +// AuthenticationGSS +// AuthenticationSSPI +// AuthenticationGSSContinue +// AuthenticationSASL +// AuthenticationSASLContinue +// AuthenticationSASLFinal +// NegotiateProtocolVersion +const AuthReq byte = 0x52 + +const NegotiateProtocolVersion = 0x76 + +func verifyPSQL(data []byte) bool { + msgLength := len(data) + if msgLength < 6 { + // from reading (https://www.postgresql.org/docs/14/protocol-message-formats.html) + // no valid server response from the startup packet can be less than 6 bytes + return false + } + + // (heuristic) Check if length of error or authentication method is reasonable + // (assume length is less than 16 bits) + if data[1] != 0 || data[2] != 0 { + return false + } + + // ErrorResponse or NegotiateProtocolVersion status codes are probably a PSQL server + if data[0] == ErrorResponse || data[0] == NegotiateProtocolVersion { + return true + } + + // A message starting with AuthReq is likely a PSQL server + if data[0] == AuthReq { + return true + } + + // Anything else is not a valid server response + return false +} + +// parse the message from the PSQL server to see if it requires AUTH +// a valid AUTH_OK message is: +// [AuthReq UINT32(8) UINT32(0)] +func successfulAuth(data []byte) bool { + msgLength := len(data) + // the AUTH_OK message is 9 bytes + if msgLength < 9 { + return false + } + if data[0] != AuthReq { + return false + } + length := binary.BigEndian.Uint32(data[1:5]) + if length != 8 { + return false + } + msg := binary.BigEndian.Uint32(data[5:9]) + return msg == 0 +} + +func init() { + plugins.RegisterPlugin(&POSTGRESPlugin{}) +} + +func (p *POSTGRESPlugin) PortPriority(port uint16) bool { + return port == 5432 +} + +func (p *POSTGRESPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + startupPacket := []byte{ + 0x00, 0x00, 0x00, 0x54, 0x00, 0x03, 0x00, 0x00, 0x75, 0x73, 0x65, 0x72, 0x00, 0x70, 0x6f, 0x73, + 0x74, 0x67, 0x72, 0x65, 0x73, 0x00, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x00, 0x70, + 0x6f, 0x73, 0x74, 0x67, 0x72, 0x65, 0x73, 0x00, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x70, 0x73, 0x71, 0x6c, 0x00, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, 0x55, 0x54, + 0x46, 0x38, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, startupPacket, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + isPSQL := verifyPSQL(response) + if !isPSQL { + return nil, nil + } + + payload := plugins.ServicePostgreSQL{ + AuthRequired: !successfulAuth(response), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *POSTGRESPlugin) Name() string { + return POSTGRES +} + +func (p *POSTGRESPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *POSTGRESPlugin) Priority() int { + return 1000 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/postgresql/postgresql_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/postgresql/postgresql_test.go new file mode 100644 index 0000000..2131307 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/postgresql/postgresql_test.go @@ -0,0 +1,47 @@ +package postgres + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestPostgreSQL(t *testing.T) { + // Flaky in CI: postgres startup/health varies and can exceed our retry window. + t.Skip("skipping flaky docker integration test") + + testcases := []test.Testcase{ + { + Description: "postgresql", + Port: 5432, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "postgres", + Env: []string{ + "POSTGRES_PASSWORD=secret", + "POSTGRES_USER=user_name", + "POSTGRES_DB=dbname", + "listen_addresses = '*'", + }, + }, + }, + } + + p := &POSTGRESPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/rdp/rdp.go b/rigour/pkg/crawler/fingerprint/plugins/services/rdp/rdp.go new file mode 100644 index 0000000..e7eedd1 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/rdp/rdp.go @@ -0,0 +1,372 @@ +package rdp + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "reflect" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type RDPPlugin struct{} +type TLSPlugin struct{} + +const RDP = "rdp" + +func init() { + plugins.RegisterPlugin(&RDPPlugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +// checkSignature checks if a given response matches the expected signature for +// the response +func checkSignature(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +func (p *RDPPlugin) PortPriority(port uint16) bool { + return port == 3389 +} + +func (p *TLSPlugin) PortPriority(port uint16) bool { + return port == 3389 +} + +// getOperatingSystemSignatures returns operating system specific signatures +// for the RDP service. +func getOperatingSystemSignatures() map[string][]byte { + Windows2000 := []byte{ + 0x03, 0x00, 0x00, 0x0b, 0x06, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, + } + + WindowsServer2003 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, + 0x03, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + WindowsServer2008 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + Windows7OrServer2008R2 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x09, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + WindowsServer2008R2DC := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x01, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + Windows10 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x1f, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + WindowsServer2012Or8 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x0f, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + WindowsServer2016or2019 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x1f, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, + } + + signatures := map[string][]byte{ + "Windows 2000": Windows2000, + "Windows Server 2003": WindowsServer2003, + "Windows Server 2008": WindowsServer2008, + "Windows 7 or Server 2008 R2": Windows7OrServer2008R2, + "Windows Server 2008 R2 DC": WindowsServer2008R2DC, + "Windows 10": Windows10, + "Windows 8 or Server 2012": WindowsServer2012Or8, + "Windows Server 2016 or 2019": WindowsServer2016or2019, + } + + return signatures +} + +// checkIsRDPGeneric leverages a generic RDP signature to identify if the +// target port is running the RDP service. +func checkRDP(response []byte) bool { + GenericRDPSignature := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, + } + + signature := GenericRDPSignature + signatureLength := len(GenericRDPSignature) + + if len(response) < signatureLength { + return false + } + + responseSlice := response[:signatureLength] + tof := checkSignature(responseSlice, signature) + return tof +} + +// guessOS tries to leverage operating system specific signatures to identify +// if the target port is running a specific operating system. +func guessOS(response []byte) (bool, string) { + signatures := getOperatingSystemSignatures() + for fingerprint, signature := range signatures { + signatureLength := len(signature) + + if len(response) < signatureLength { + continue + } + + responseSlice := response[:signatureLength] + tof := checkSignature(responseSlice, signature) + if tof { + return true, fingerprint + } + } + + return false, "" +} + +func DetectRDP(conn net.Conn, timeout time.Duration) (string, bool, error) { + InitialConnectionPacket := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xe0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x0b, + 0x00, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, InitialConnectionPacket, timeout) + if err != nil { + return "", false, err + } + if len(response) == 0 { + return "", true, &utils.ServerNotEnable{} + } + + isRDP := checkRDP(response) + fingerprint := "" + if isRDP { + success, osFingerprint := guessOS(response) + if success { + fingerprint = osFingerprint + } + + return fingerprint, true, nil + } + return "", true, &utils.InvalidResponseError{Service: RDP} +} + +func DetectRDPAuth(conn net.Conn, timeout time.Duration) (*plugins.ServiceRDP, bool, error) { + info := plugins.ServiceRDP{} + + // CredSSP protocol - NTLM authentication + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp + // http://davenport.sourceforge.net/ntlm.html + + NegotiatePacket := []byte{ + 0x30, 0x37, 0xA0, 0x03, 0x02, 0x01, 0x60, 0xA1, 0x30, 0x30, 0x2E, 0x30, 0x2C, 0xA0, 0x2A, 0x04, 0x28, + // Signature + 'N', 'T', 'L', 'M', 'S', 'S', 'P', 0x00, + // Message Type + 0x01, 0x00, 0x00, 0x00, + // Negotiate Flags + 0xF7, 0xBA, 0xDB, 0xE2, + // Domain Name Fields + 0x00, 0x00, // DomainNameLen + 0x00, 0x00, // DomainNameMaxLen + 0x00, 0x00, 0x00, 0x00, // DomainNameBufferOffset + // Workstation Fields + 0x00, 0x00, // WorkstationLen + 0x00, 0x00, // WorkstationMaxLen + 0x00, 0x00, 0x00, 0x00, // WorkstationBufferOffset + // Version + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, NegotiatePacket, timeout) + if err != nil { + return nil, false, err + } + + type NTLMChallenge struct { + Signature [8]byte + MessageType uint32 + TargetNameLen uint16 + TargetNameMaxLen uint16 + TargetNameBufferOffset uint32 + NegotiateFlags uint32 + ServerChallenge uint64 + Reserved uint64 + TargetInfoLen uint16 + TargetInfoMaxLen uint16 + TargetInfoBufferOffset uint32 + Version [8]byte + // Payload (variable) + } + var challengeLen = 56 + + challengeStartOffset := bytes.Index(response, []byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0}) + if challengeStartOffset == -1 { + return nil, false, nil + } + if len(response) < challengeStartOffset+challengeLen { + return nil, false, nil + } + var responseData NTLMChallenge + response = response[challengeStartOffset:] + responseBuf := bytes.NewBuffer(response) + err = binary.Read(responseBuf, binary.LittleEndian, &responseData) + if err != nil { + return nil, false, err + } + + // Check if valid NTLM challenge response message structure + if responseData.MessageType != 0x00000002 || + responseData.Reserved != 0 || + !reflect.DeepEqual(responseData.Version[4:], []byte{0, 0, 0, 0xF}) { + return nil, false, nil + } + + // Parse: Version + type version struct { + MajorVersion byte + MinorVersion byte + BuildNumber uint16 + } + var versionData version + versionBuf := bytes.NewBuffer(responseData.Version[:4]) + err = binary.Read(versionBuf, binary.LittleEndian, &versionData) + if err != nil { + return nil, true, err + } + info.OSVersion = fmt.Sprintf("%d.%d.%d", versionData.MajorVersion, + versionData.MinorVersion, + versionData.BuildNumber) + + // Parse: TargetName + targetNameLen := int(responseData.TargetNameLen) + if targetNameLen > 0 { + startIdx := int(responseData.TargetNameBufferOffset) + endIdx := startIdx + targetNameLen + targetName := strings.ReplaceAll(string(response[startIdx:endIdx]), "\x00", "") + info.TargetName = targetName + } + + // Parse: TargetInfo + AvIDMap := map[uint16]string{ + 1: "NetBIOSComputerName", + 2: "NetBIOSDomainName", + 3: "FQDN", // DNS Computer Name + 4: "DNSDomainName", + 5: "DNSTreeName", + } + + type AVPair struct { + AvID uint16 + AvLen uint16 + } + var avPairLen = 4 + targetInfoLen := int(responseData.TargetInfoLen) + if targetInfoLen > 0 { + startIdx := int(responseData.TargetInfoBufferOffset) + if startIdx+targetInfoLen > len(response) { + return &info, true, fmt.Errorf("Invalid TargetInfoLen value") + } + var avPair AVPair + avPairBuf := bytes.NewBuffer(response[startIdx : startIdx+avPairLen]) + err = binary.Read(avPairBuf, binary.LittleEndian, &avPair) + if err != nil { + return &info, true, err + } + currIdx := startIdx + for avPair.AvID != 0 { + if field, exists := AvIDMap[avPair.AvID]; exists { + value := strings.ReplaceAll(string(response[currIdx+avPairLen:currIdx+avPairLen+int(avPair.AvLen)]), "\x00", "") + switch field { + case "netbiosComputerName": + info.NetBIOSComputerName = value + case "netbiosDomainName": + info.NetBIOSDomainName = value + case "dnsComputerName": + info.DNSComputerName = value + case "dnsDomainName": + info.DNSDomainName = value + case "forestName": // MsvAvDnsTreeName + info.ForestName = value + } + } + currIdx += avPairLen + int(avPair.AvLen) + if currIdx+avPairLen > startIdx+targetInfoLen { + return &info, true, fmt.Errorf("Invalid AV_PAIR list") + } + avPairBuf = bytes.NewBuffer(response[currIdx : currIdx+avPairLen]) + err = binary.Read(avPairBuf, binary.LittleEndian, &avPair) + if err != nil { + return &info, true, err + } + } + } + + return &info, true, nil +} + +func (p *RDPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + fingerprint, check, err := DetectRDP(conn, timeout) + if check && err != nil { + return nil, nil + } else if check && err == nil { + payload := plugins.ServiceRDP{ + OSFingerprint: fingerprint, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + return nil, err +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + info, check, err := DetectRDPAuth(conn, timeout) + if check && err != nil { + return nil, nil + } else if check && info != nil && err == nil { + return plugins.CreateServiceFrom(target, *info, true, "", plugins.TCP), nil + } + return nil, err +} + +func (p *RDPPlugin) Name() string { + return RDP +} + +func (p *RDPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Name() string { + return RDP +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *RDPPlugin) Priority() int { + return 89 +} + +func (p *TLSPlugin) Priority() int { + return 89 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/rdp/rdp_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/rdp/rdp_test.go new file mode 100644 index 0000000..635b31b --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/rdp/rdp_test.go @@ -0,0 +1,38 @@ +package rdp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestRDP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "rdp", + Port: 3389, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "scottyhardy/docker-remote-desktop", + }, + }, + } + + p := &RDPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/redis/redis.go b/rigour/pkg/crawler/fingerprint/plugins/services/redis/redis.go new file mode 100644 index 0000000..ff69cc3 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/redis/redis.go @@ -0,0 +1,144 @@ +package redis + +import ( + "bytes" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type REDISPlugin struct{} +type REDISTLSPlugin struct{} + +type Info struct { + AuthRequired bool +} + +const REDIS = "redis" +const REDISTLS = "redis" + +// Check if the response is from a Redis server +// returns an error if it's not validated as a Redis server +// and a Info struct with AuthRequired if it is +func checkRedis(data []byte) (Info, error) { + // a valid pong response will be the 7 bytes [+PONG(CR)(NL)] + pong := [7]byte{0x2b, 0x50, 0x4f, 0x4e, 0x47, 0x0d, 0x0a} + // an auth error will start with the 7 bytes: [-NOAUTH] + noauth := [7]byte{0x2d, 0x4e, 0x4f, 0x41, 0x55, 0x54, 0x48} + + msgLength := len(data) + if msgLength < 7 { + return Info{}, &utils.InvalidResponseErrorInfo{ + Service: REDIS, + Info: "too short of a response", + } + } + + if msgLength == 7 { + if bytes.Equal(data, pong[:]) { + // Valid PONG response means redis server and no auth + return Info{AuthRequired: false}, nil + } + return Info{}, &utils.InvalidResponseErrorInfo{ + Service: REDIS, + Info: "invalid PONG response", + } + } + if !bytes.Equal(data[:7], noauth[:]) { + return Info{}, &utils.InvalidResponseErrorInfo{ + Service: REDIS, + Info: "invalid Error response", + } + } + + return Info{AuthRequired: true}, nil +} + +func init() { + plugins.RegisterPlugin(&REDISPlugin{}) + plugins.RegisterPlugin(&REDISTLSPlugin{}) +} + +func (p *REDISPlugin) PortPriority(port uint16) bool { + return port == 6379 +} + +func (p *REDISTLSPlugin) PortPriority(port uint16) bool { + return port == 6380 +} + +func (p *REDISTLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return DetectRedis(conn, target, timeout, true) +} + +func DetectRedis(conn net.Conn, target plugins.Target, timeout time.Duration, tls bool) (*plugins.Service, error) { + //https://redis.io/commands/ping/ + // PING is a supported command since 1.0.0 + // [*1(CR)(NL)$4(CR)(NL)PING(CR)(NL)] + ping := []byte{ + 0x2a, + 0x31, + 0x0d, + 0x0a, + 0x24, + 0x34, + 0x0d, + 0x0a, + 0x50, + 0x49, + 0x4e, + 0x47, + 0x0d, + 0x0a, + } + + response, err := utils.SendRecv(conn, ping, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + result, err := checkRedis(response) + if err != nil { + return nil, nil + } + payload := plugins.ServiceRedis{ + AuthRequired: result.AuthRequired, + } + if tls { + return plugins.CreateServiceFrom(target, payload, true, "", plugins.TCPTLS), nil + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *REDISPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return DetectRedis(conn, target, timeout, false) +} + +func (p *REDISPlugin) Name() string { + return REDIS +} + +func (p *REDISTLSPlugin) Name() string { + return REDISTLS +} + +func (p *REDISPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *REDISTLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *REDISPlugin) Priority() int { + return 413 +} + +func (p *REDISTLSPlugin) Priority() int { + return 414 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/redis/redis_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/redis/redis_test.go new file mode 100644 index 0000000..6613393 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/redis/redis_test.go @@ -0,0 +1,38 @@ +package redis + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestRedis(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "redis", + Port: 6379, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "redis", + }, + }, + } + + p := &REDISPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/rsync/rsync.go b/rigour/pkg/crawler/fingerprint/plugins/services/rsync/rsync.go new file mode 100644 index 0000000..efccea4 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/rsync/rsync.go @@ -0,0 +1,77 @@ +package rsync + +import ( + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type RSYNCPlugin struct{} + +const ( + RsyncMagicHeaderLength = 8 + RSYNC = "rsync" +) + +func init() { + plugins.RegisterPlugin(&RSYNCPlugin{}) +} + +func (p *RSYNCPlugin) PortPriority(port uint16) bool { + return port == 873 +} + +// Run +/* + rsync is a file synchronization protocol that can run over a number of protocols. Once + a communication stream is set up between the sender and receiver processes, the protocol is the same, regardless + of whether that stream is a unix pipe, an SSH connection, or a raw TCP socket. This program detects the + presence of an rsync daemon, which detects incoming connections and forks to use a raw TCP socket. The + rsync daemon uses no transport encryption. + + The rsync protocol is not standardized, but all implementations use a magic header "@RSYNCD:" during synchronization. + + This program was tested with docker run -p 873:873 vimagick/rsyncd + The default port for rsyncd is 873 +*/ +func (p *RSYNCPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + requestBytes := []byte{ + // ascii "@RSYNCD:" magic header + 0x40, 0x52, 0x53, 0x59, 0x54, 0x43, 0x44, 0x3a, + // space + 0x20, + // ascii "29" client version + 0x32, 0x39, + // newline + 0x0a, + } + + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + if string(response[:RsyncMagicHeaderLength]) == "@RSYNCD:" { + version := strings.Split(string(response[RsyncMagicHeaderLength+1:]), "\n")[0] + return plugins.CreateServiceFrom(target, plugins.ServiceRsync{}, false, version, plugins.TCP), nil + } + + return nil, nil +} + +func (p *RSYNCPlugin) Name() string { + return RSYNC +} + +func (p *RSYNCPlugin) Type() plugins.Protocol { + return plugins.TCP +} +func (p *RSYNCPlugin) Priority() int { + return 578 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/rsync/rsync_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/rsync/rsync_test.go new file mode 100644 index 0000000..cdec645 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/rsync/rsync_test.go @@ -0,0 +1,38 @@ +package rsync + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestRsync(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "rsync", + Port: 873, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "vimagick/rsyncd", + }, + }, + } + + p := &RSYNCPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/rtsp/rtsp.go b/rigour/pkg/crawler/fingerprint/plugins/services/rtsp/rtsp.go new file mode 100644 index 0000000..8766fec --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/rtsp/rtsp.go @@ -0,0 +1,113 @@ +package rtsp + +import ( + "math/rand" + "net" + "strconv" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const ( + RtspMagicHeader = "RTSP/1.0" + RtspMagicHeaderLength = 8 + RtspCseqHeader = "CSeq: " + RtspCseqHeaderLength = 6 + RtspServerHeader = "Server: " + RtspServerHeaderLength = 8 + RtspNewlineLength = 2 + RTSP = "rtsp" +) + +type RTSPPlugin struct{} + +func init() { + rand.Seed(time.Now().UnixNano()) + plugins.RegisterPlugin(&RTSPPlugin{}) +} + +func (p *RTSPPlugin) PortPriority(port uint16) bool { + return port == 554 +} + +/* + rtsp is a media control protocol used to control the flow of data from a real time + data streaming protocol. rtsp itself does not transport any data. The structure of rtsp + requests is very similar to that of http requests. + + To detect the presence of RTSP, this program sends an OPTIONS request, and then validates + the returned header and cseq value. + + This program was tested with docker run -p 554:8554 aler9/rtsp-simple-server. + The default port for rtsp is 554. +*/ + +func (p *RTSPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + cseq := strconv.Itoa(rand.Intn(10000)) //nolint:gosec + + requestString := strings.Join([]string{ + "OPTIONS rtsp://example.com RTSP/1.0\r\n", + "Cseq: ", cseq, "\r\n", + "\r\n", + }, "") + + requestBytes := []byte(requestString) + + responseBytes, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return nil, err + } + if len(responseBytes) == 0 { + return nil, nil + } + response := string(responseBytes) + + if len(response) < RtspMagicHeaderLength { + return nil, nil + } + if string(response[:RtspMagicHeaderLength]) == RtspMagicHeader { + cseqStart := strings.Index(response, RtspCseqHeader) + if cseqStart == -1 { + return nil, nil + } + + cseqValueStart := cseqStart + RtspCseqHeaderLength + if response[cseqValueStart:cseqValueStart+len(cseq)+RtspNewlineLength] != cseq+"\r\n" { + return nil, nil + } + + serverStart := strings.Index(response, RtspServerHeader) + if serverStart == -1 { + return nil, nil + } + + serverValueStart := serverStart + RtspServerHeaderLength + serverValueEnd := strings.Index(response[serverValueStart:], "\r\n") + if serverValueStart+serverValueEnd >= len(response) { + return nil, nil + } + + serverinfo := response[serverValueStart : serverValueStart+serverValueEnd] + payload := plugins.ServiceRtsp{ + ServerInfo: serverinfo, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + + return nil, nil +} + +func (p *RTSPPlugin) Name() string { + return RTSP +} + +func (p *RTSPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *RTSPPlugin) Priority() int { + return 1001 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/rtsp/rtsp_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/rtsp/rtsp_test.go new file mode 100644 index 0000000..292427f --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/rtsp/rtsp_test.go @@ -0,0 +1,44 @@ +package rtsp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestRtsp(t *testing.T) { + // This test depends on Docker image behavior and readiness on the host. + // It has proven flaky in CI and local environments (slow startup / probe mismatch), + // so we skip it to keep `go test ./...` deterministic. + t.Skip("skipping RTSP docker integration test (flaky in CI)") + + testcases := []test.Testcase{ + { + Description: "rtsp", + Port: 8554, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "aler9/rtsp-simple-server", + ExposedPorts: []string{"8554/tcp"}, + }, + }, + } + + p := &RTSPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/smb/smb.go b/rigour/pkg/crawler/fingerprint/plugins/services/smb/smb.go new file mode 100644 index 0000000..97b01d0 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/smb/smb.go @@ -0,0 +1,361 @@ +package smb + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "net" + "reflect" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type SMBPlugin struct{} + +const SMB = "smb" + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5cd64522-60b3-4f3e-a157-fe66f1228052 +type SMB2PacketHeader struct { + ProtocolID [4]byte + StructureSize uint16 + CreditCharge uint16 + Status uint32 // In SMB 3.x dialect, used as ChannelSequence & Reserved fields + Command uint16 + CreditRequest uint16 + Flags uint32 + NextCommand uint32 + MessageID uint64 + Reserved uint32 + TreeID uint32 + SessionID uint64 + Signature [16]byte +} + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/63abf97c-0d09-47e2-88d6-6bfa552949a5 +type NegotiateResponse struct { + SessionMsgPrefix [4]byte + PacketHeader SMB2PacketHeader + // Negotiate Response + StructureSize uint16 + SecurityMode uint16 + DialectRevision uint16 + Reserved uint16 // if DialectRevision is 0x0311, used as NegotiateContextCount field + ServerGUID [16]byte + Capabilities uint32 + MaxTransactSize uint32 + MaxReadSize uint32 + MaxWriteSize uint32 + SystemTime uint64 + ServerStartTime uint64 + SecurityBufferOffset uint16 + SecurityBufferLength uint16 + Reserved2 uint32 // if DialectRevision is 0x0311, used as NegotiateContextOffset field + // Variable (Buffer, Padding, NegotiateContextList, etc.) +} + +type NTLMChallenge struct { + Signature [8]byte + MessageType uint32 + TargetNameLen uint16 + TargetNameMaxLen uint16 + TargetNameBufferOffset uint32 + NegotiateFlags uint32 + ServerChallenge uint64 + Reserved uint64 + TargetInfoLen uint16 + TargetInfoMaxLen uint16 + TargetInfoBufferOffset uint32 + Version [8]byte + // Payload (variable) +} + +func init() { + plugins.RegisterPlugin(&SMBPlugin{}) +} + +func (p *SMBPlugin) PortPriority(port uint16) bool { + return port == 445 +} + +func DetectSMBv2(conn net.Conn, timeout time.Duration) (*plugins.ServiceSMB, error) { + info := plugins.ServiceSMB{} + + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e14db7ff-763a-4263-8b10-0c3944f52fc5 + negotiateReqPacket := []byte{ + // NetBios Session Service + 0x00, // Message Type + 0x00, 0x00, 0x66, // Length + + // SMBv2 Packet Header + 0xFE, 0x53, 0x4D, 0x42, // ProtocolId + 0x40, 0x00, // StructureSize + 0x00, 0x00, // CreditCharge + 0x00, 0x00, 0x00, 0x00, // ChannelSequence/Reserved/Status + 0x00, 0x00, // Command (Negotiate) + 0x00, 0x1F, // CreditRequest + 0x00, 0x00, 0x00, 0x00, // Flags + 0x00, 0x00, 0x00, 0x00, // NextCommand + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MessageID + 0x00, 0x00, 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, // TreeID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // SessionID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Signature + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Signature (continued) + + // SMBv2 Negotiate Request + 0x24, 0x00, // StructureSize + 0x01, 0x00, // DialectCount + 0x01, 0x00, // SecurityMode (Signing Enabled) + 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, // Capabilities + 0x13, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, // ClientGuid + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x37, // ClientGuid (continued) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ClientStartTime + 0x02, 0x02, // Dialects (SMB 2.0.2) + } + sessionPrefixLen := 4 + packetHeaderLen := 64 + minNegoResponseLen := 64 + + response, err := utils.SendRecv(conn, negotiateReqPacket, timeout) + if err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return nil, nil + } + return nil, err + } + + // Check the length of the response to see if it is lower than the minimum + // packet size for SMB2 NEGOTIATE Response Packet + if len(response) < sessionPrefixLen+packetHeaderLen+minNegoResponseLen { + return nil, nil + } + + var negotiateResponseData NegotiateResponse + responseBuf := bytes.NewBuffer(response) + err = binary.Read(responseBuf, binary.LittleEndian, &negotiateResponseData) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(negotiateResponseData.PacketHeader.ProtocolID[:], []byte{0xFE, 'S', 'M', 'B'}) { + return nil, nil + } + + if negotiateResponseData.PacketHeader.StructureSize != 0x40 { + return nil, nil + } + + if negotiateResponseData.PacketHeader.Command != 0x0000 { // SMB2 NEGOTIATE (0x0000) + return nil, nil + } + + if negotiateResponseData.StructureSize != 0x41 { + return nil, nil + } + + signingEnabled := false + signingRequired := false + if negotiateResponseData.SecurityMode&1 == 1 { + signingEnabled = true + } + if negotiateResponseData.SecurityMode&2 == 2 { + signingRequired = true + } + info.SigningEnabled = signingEnabled + info.SigningRequired = signingRequired + + /** + * At this point, we know SMBv2 is detected. + * Below, we try to obtain more metadata via session setup request w/ NTLM auth + */ + + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-authsod/9a20f8ac-612a-4e0a-baab-30e922e7e1f5 + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a3c2c28-d6b0-48ed-b917-a86b2ca4575f + sessionSetupReqPacket := []byte{ + // NetBios Session Service + 0x00, // Message Type + 0x00, 0x00, 0xA2, // Length + + // SMBv2 Packet Header + 0xFE, 0x53, 0x4D, 0x42, // ProtocolId + 0x40, 0x00, // StructureSize + 0x00, 0x00, // CreditCharge + 0x00, 0x00, 0x00, 0x00, // ChannelSequence/Reserved/Status + 0x01, 0x00, // Command (SESSION_SETUP) + 0x00, 0x20, // CreditRequest + 0x00, 0x00, 0x00, 0x00, // Flags + 0x00, 0x00, 0x00, 0x00, // NextCommand + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MessageID + 0x00, 0x00, 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, // TreeID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // SessionID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Signature + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Signature (continued) + + // SMBv2 Session Setup Request + 0x19, 0x00, // Structure Size + 0x00, // Flags + 0x01, // SecurityMode + 0x01, 0x00, 0x00, 0x00, // Capabilities + 0x00, 0x00, 0x00, 0x00, // Channel + 0x58, 0x00, // SecurityBufferOffset + 0x4A, 0x00, // SecurityBufferLength + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // PreviousSessionId + // Security Buffer + 0x60, 0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, + 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, + 0x30, 0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, + 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04, 0x28, + // Signature + 'N', 'T', 'L', 'M', 'S', 'S', 'P', 0x00, + // Message Type + 0x01, 0x00, 0x00, 0x00, + // Negotiate Flags + 0xF7, 0xBA, 0xDB, 0xE2, + // Domain Name Fields + 0x00, 0x00, // DomainNameLen + 0x00, 0x00, // DomainNameMaxLen + 0x00, 0x00, 0x00, 0x00, // DomainNameBufferOffset + // Workstation Fields + 0x00, 0x00, // WorkstationLen + 0x00, 0x00, // WorkstationMaxLen + 0x00, 0x00, 0x00, 0x00, // WorkstationBufferOffset + // Version + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + + response, err = utils.SendRecv(conn, sessionSetupReqPacket, timeout) + if err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return &info, nil + } + return &info, err + } + + challengeLen := 56 + challengeStartOffset := bytes.Index(response, []byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0}) + if challengeStartOffset == -1 { + return &info, nil + } + if len(response) < challengeStartOffset+challengeLen { + return &info, nil + } + var sessionResponseData NTLMChallenge + response = response[challengeStartOffset:] + responseBuf = bytes.NewBuffer(response) + err = binary.Read(responseBuf, binary.LittleEndian, &sessionResponseData) + if err != nil { + return &info, err + } + + // Check if valid NTLM challenge response message structure + if sessionResponseData.MessageType != 0x00000002 || + sessionResponseData.Reserved != 0 || + !reflect.DeepEqual(sessionResponseData.Version[4:], []byte{0, 0, 0, 0xF}) { + return &info, nil + } + + // Parse: Version + type version struct { + MajorVersion byte + MinorVersion byte + BuildNumber uint16 + } + var versionData version + versionBuf := bytes.NewBuffer(sessionResponseData.Version[:4]) + err = binary.Read(versionBuf, binary.LittleEndian, &versionData) + if err != nil { + return &info, err + } + info.OSVersion = fmt.Sprintf("%d.%d.%d", versionData.MajorVersion, + versionData.MinorVersion, + versionData.BuildNumber) + + // Parse: TargetInfo + AvIDMap := map[uint16]string{ + 1: "netbiosComputerName", + 2: "netbiosDomainName", + 3: "dnsComputerName", + 4: "dnsDomainName", + 5: "forestName", // MsvAvDnsTreeName + } + type AVPair struct { + AvID uint16 + AvLen uint16 + // Value (variable) + } + var avPairLen = 4 + targetInfoLen := int(sessionResponseData.TargetInfoLen) + if targetInfoLen > 0 { + startIdx := int(sessionResponseData.TargetInfoBufferOffset) + if startIdx+targetInfoLen > len(response) { + return &info, nil + } + var avPair AVPair + avPairBuf := bytes.NewBuffer(response[startIdx : startIdx+avPairLen]) + err = binary.Read(avPairBuf, binary.LittleEndian, &avPair) + if err != nil { + return &info, err + } + currIdx := startIdx + for avPair.AvID != 0 { + if field, exists := AvIDMap[avPair.AvID]; exists { + value := strings.ReplaceAll(string(response[currIdx+avPairLen:currIdx+avPairLen+int(avPair.AvLen)]), "\x00", "") + switch field { + case "netbiosComputerName": + info.NetBIOSComputerName = value + case "netbiosDomainName": + info.NetBIOSDomainName = value + case "dnsComputerName": + info.DNSComputerName = value + case "dnsDomainName": + info.DNSDomainName = value + case "forestName": // MsvAvDnsTreeName + info.ForestName = value + } + } + currIdx += avPairLen + int(avPair.AvLen) + if currIdx+avPairLen > startIdx+targetInfoLen { + return &info, nil + } + avPairBuf = bytes.NewBuffer(response[currIdx : currIdx+avPairLen]) + err = binary.Read(avPairBuf, binary.LittleEndian, &avPair) + if err != nil { + return &info, nil + } + } + } + + return &info, nil +} + +func (p *SMBPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + info, err := DetectSMBv2(conn, timeout) + if err != nil { + return nil, err + } + if info == nil { + return nil, nil + } + + return plugins.CreateServiceFrom(target, info, false, info.OSVersion, plugins.TCP), nil +} + +func (p *SMBPlugin) Name() string { + return SMB +} + +func (p *SMBPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *SMBPlugin) Priority() int { + return 320 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/smb/smb_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/smb/smb_test.go new file mode 100644 index 0000000..88af293 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/smb/smb_test.go @@ -0,0 +1,39 @@ +package smb + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestSMB(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "smb", + Port: 445, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "dperson/samba", + Cmd: []string{"-S"}, + }, + }, + } + + p := &SMBPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/smtp/smtp.go b/rigour/pkg/crawler/fingerprint/plugins/services/smtp/smtp.go new file mode 100644 index 0000000..b54cfc3 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/smtp/smtp.go @@ -0,0 +1,188 @@ +package smtp + +import ( + "bytes" + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type SMTPPlugin struct{} +type TLSPlugin struct{} + +const SMTP = "smtp" +const SMTPS = "smtps" + +type Data struct { + Banner string + AuthMethods []string +} + +func init() { + plugins.RegisterPlugin(&SMTPPlugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func (p *SMTPPlugin) PortPriority(port uint16) bool { + return port == 25 || port == 587 || port == 465 || port == 2525 +} + +func handleSMTPConn(response []byte) (bool, bool) { + // Checks for an expected response on CONNECTION ESTABLISHMENT + // RFC 5321 Section 4.3.2 + validResponses := []string{"220", "421", "500", "501", "554"} + isSMTP := false + isSMTPErr := false + for i := 0; i < len(validResponses); i++ { + if bytes.Equal(response[0:3], []byte(validResponses[i])) { + // Received a valid response code on connection + isSMTP = true + if bytes.Equal(response[0:1], []byte("4")) || bytes.Equal(response[0:1], []byte("5")) { + // Received a valid error response code on connection + isSMTPErr = true + } + break + } + } + return isSMTP, isSMTPErr +} + +func handleSMTPHelo(response []byte) (bool, bool) { + // Checks for an expected response from the HELO command + // RFC 5321 Section 4.3.2 + validResponses := []string{"250", "421", "500", "501", "502", "504", "550"} + isSMTP := false + isSMTPErr := false + for i := 0; i < len(validResponses); i++ { + if bytes.Equal(response[0:3], []byte(validResponses[i])) { + // HELO command received a valid response code + isSMTP = true + if bytes.Equal(response[0:1], []byte("4")) || bytes.Equal(response[0:1], []byte("5")) { + // HELO command received a valid error response code + isSMTPErr = true + } + break + } + } + return isSMTP, isSMTPErr +} + +func (p *TLSPlugin) PortPriority(port uint16) bool { + return port == 465 +} + +func DetectSMTP(conn net.Conn, tls bool, timeout time.Duration) (Data, bool, error) { + protocol := SMTP + if tls { + protocol = SMTPS + } + + response, err := utils.Recv(conn, timeout) + if err != nil { + return Data{}, false, err + } + if len(response) == 0 { + return Data{}, true, &utils.ServerNotEnable{} + } + + isSMTP, smtpError := handleSMTPConn(response) + if !isSMTP && !smtpError { + return Data{}, true, &utils.InvalidResponseError{Service: protocol} + } + + banner := make([]byte, len(response)) + copy(banner, response) + + // Send the EHLO message + smtpEhloCommand := []byte("EHLO example.com\r\n") + response, err = utils.SendRecv(conn, smtpEhloCommand, timeout) + if err != nil { + return Data{}, false, err + } + if len(response) == 0 { + return Data{}, true, &utils.ServerNotEnable{} + } + + isSMTP, smtpError = handleSMTPHelo(response) + if !isSMTP { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: protocol, + Info: "invalid SMTP Helo response", + } + } + + // a valid smtperror means it is smtp + if smtpError { + data := Data{ + Banner: string(banner), + } + + return data, true, nil + } + + if isSMTP { + data := Data{ + Banner: string(banner), + AuthMethods: strings.Split(strings.ReplaceAll(string(response), "-", " "), " "), + } + + return data, true, nil + } + + return Data{}, true, &utils.InvalidResponseError{Service: protocol} +} + +func (p *SMTPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + data, check, err := DetectSMTP(conn, false, timeout) + if err == nil && check { + payload := plugins.ServiceSMTP{ + Banner: data.Banner, + AuthMethods: data.AuthMethods, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } else if err != nil && check { + return nil, nil + } + return nil, err +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + data, check, err := DetectSMTP(conn, false, timeout) + if err == nil && check { + payload := plugins.ServiceSMTP{ + Banner: data.Banner, + AuthMethods: data.AuthMethods, + } + return plugins.CreateServiceFrom(target, payload, true, "", plugins.TCP), nil + } else if err != nil && check { + return nil, nil + } + return nil, err +} + +func (p *SMTPPlugin) Name() string { + return SMTP +} + +func (p *SMTPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Name() string { + return SMTPS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *SMTPPlugin) Priority() int { + return 60 +} + +func (p *TLSPlugin) Priority() int { + return 61 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/smtp/smtp_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/smtp/smtp_test.go new file mode 100644 index 0000000..6fc7337 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/smtp/smtp_test.go @@ -0,0 +1,38 @@ +package smtp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestSMTP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "smtp", + Port: 25, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "bytemark/smtp", + }, + }, + } + + p := &SMTPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/snmp/snmp.go b/rigour/pkg/crawler/fingerprint/plugins/services/snmp/snmp.go new file mode 100644 index 0000000..4edd33c --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/snmp/snmp.go @@ -0,0 +1,75 @@ +package snmp + +import ( + "bytes" + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const SNMP = "SNMP" + +type SNMPPlugin struct{} + +func init() { + plugins.RegisterPlugin(&SNMPPlugin{}) +} + +func (f *SNMPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + RequestID := []byte{0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00} + InitialConnectionPackage := []byte{ + 0x30, 0x29, // package length + 0x02, 0x01, 0x00, // Version: 1 + 0x04, 0x06, // Community + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // method: "public" + 0xa0, // PDU type: GET + 0x1c, + 0x02, 0x04, 0xff, 0xff, 0xff, 0xff, // Request ID: -1 + 0x02, 0x01, 0x00, // Error status: no error + 0x02, 0x01, 0x00, // Error index + 0x30, 0x0e, 0x30, 0x0c, 0x06, 0x08, 0x2b, 0x06, // Object ID + 0x01, 0x02, 0x01, 0x01, 0x01, 0x00, 0x05, 0x00, + } + InfoOffset := 33 + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + idx := strings.Index(string(response), "public") + if idx == -1 { + return nil, nil + } + stringBegin := idx + InfoOffset + if bytes.Contains(response, RequestID) { + if stringBegin < len(response) { + return plugins.CreateServiceFrom(target, plugins.ServiceSNMP{}, false, + string(response[stringBegin:]), plugins.UDP), nil + } + return plugins.CreateServiceFrom(target, plugins.ServiceSNMP{}, false, "", plugins.UDP), nil + } + return nil, nil +} + +func (f *SNMPPlugin) Name() string { + return SNMP +} + +func (f *SNMPPlugin) PortPriority(i uint16) bool { + return i == 161 +} + +func (f *SNMPPlugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (f *SNMPPlugin) Priority() int { + return 81 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/snmp/snmp_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/snmp/snmp_test.go new file mode 100644 index 0000000..60f2400 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/snmp/snmp_test.go @@ -0,0 +1,39 @@ +package snmp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestSNMP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "snmp", + Port: 161, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "polinux/snmpd", + ExposedPorts: []string{"161/udp"}, + }, + }, + } + + p := &SNMPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ssh/ssh.go b/rigour/pkg/crawler/fingerprint/plugins/services/ssh/ssh.go new file mode 100644 index 0000000..f2b66a1 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ssh/ssh.go @@ -0,0 +1,392 @@ +package ssh + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + "math/big" + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" + "github.com/ctrlsam/rigour/third_party/cryptolib/ssh" +) + +type SSHPlugin struct{} + +const SSH = "ssh" + +func init() { + plugins.RegisterPlugin(&SSHPlugin{}) +} + +func (p *SSHPlugin) PortPriority(port uint16) bool { + return port == 22 || port == 2222 +} + +// https://www.rfc-editor.org/rfc/rfc4253.html#section-4 +// from the RFC, two things: +// When the connection has been established, both sides MUST send an +// identification string. This identification string MUST be +// +// SSH-protoversion-softwareversion SP comments CR LF +// +// The server MAY send other lines of data before sending the version +// +// string. Each line SHOULD be terminated by a Carriage Return and Line +// Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded +// in ISO-10646 UTF-8 [RFC3629] (language is not specified). +func checkSSH(data []byte) (string, error) { + msgLength := len(data) + if msgLength < 4 { + return "", &utils.InvalidResponseErrorInfo{Service: SSH, Info: "response too short"} + } + sshID := []byte("SSH-") + if bytes.Equal(data[:4], sshID) { + return string(data), nil + } + + for _, line := range strings.Split(string(data), "\r\n") { + if len(line) >= 4 && line[:4] == "SSH-" { + return line, nil + } + } + + return "", &utils.InvalidResponseErrorInfo{Service: SSH, Info: "invalid banner prefix"} +} + +func checkAlgo(data []byte) (map[string]string, error) { + length := len(data) + if length < 26 { + return nil, fmt.Errorf("invalid response length") + } + cookie := hex.EncodeToString(data[6:22]) + + kexAlgorithmsLength := int(big.NewInt(0).SetBytes(data[22:26]).Uint64()) + if length < 26+kexAlgorithmsLength { + return nil, fmt.Errorf("invalid response length") + } + kexAlgos := string(data[26 : 26+kexAlgorithmsLength]) + + sHKAlgoBegin := 26 + kexAlgorithmsLength + if length < 4+sHKAlgoBegin { + return nil, fmt.Errorf("invalid response length") + } + sHKAlgoLength := int(big.NewInt(0).SetBytes(data[sHKAlgoBegin : 4+sHKAlgoBegin]).Uint64()) + if length < 4+sHKAlgoBegin+sHKAlgoLength { + return nil, fmt.Errorf("invalid response length") + } + serverHostKeyAlgos := string(data[4+sHKAlgoBegin : 4+sHKAlgoBegin+sHKAlgoLength]) + + encryptAlgoCToSBegin := 4 + sHKAlgoBegin + sHKAlgoLength + if length < 4+encryptAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + encryptAlgoCToSLength := int(big.NewInt(0).SetBytes(data[encryptAlgoCToSBegin : 4+encryptAlgoCToSBegin]).Uint64()) + if length < 4+encryptAlgoCToSBegin+encryptAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + ciphersClientServer := string(data[4+encryptAlgoCToSBegin : 4+encryptAlgoCToSBegin+encryptAlgoCToSLength]) + + encryptAlgoSToCBegin := 4 + encryptAlgoCToSBegin + encryptAlgoCToSLength + if length < 4+encryptAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + encryptAlgoSToCLength := int(big.NewInt(0).SetBytes(data[encryptAlgoSToCBegin : 4+encryptAlgoSToCBegin]).Uint64()) + if length < 4+encryptAlgoCToSBegin+encryptAlgoSToCLength { + return nil, fmt.Errorf("invalid response length") + } + ciphersServerClient := string(data[4+encryptAlgoSToCBegin : 4+encryptAlgoSToCBegin+encryptAlgoSToCLength]) + + macAlgoCToSBegin := 4 + encryptAlgoSToCBegin + encryptAlgoSToCLength + if length < 4+macAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + macAlgoCToSLength := int(big.NewInt(0).SetBytes(data[macAlgoCToSBegin : 4+macAlgoCToSBegin]).Uint64()) + if length < 4+macAlgoCToSBegin+macAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + macClientServer := string(data[4+macAlgoCToSBegin : 4+macAlgoCToSBegin+macAlgoCToSLength]) + + macAlgoSToCBegin := 4 + macAlgoCToSBegin + macAlgoCToSLength + if length < 4+macAlgoSToCBegin { + return nil, fmt.Errorf("invalid response length") + } + macAlgoSToCLength := int(big.NewInt(0).SetBytes(data[macAlgoSToCBegin : 4+macAlgoSToCBegin]).Uint64()) + if length < 4+macAlgoSToCBegin+macAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + macServerClient := string(data[4+macAlgoSToCBegin : 4+macAlgoSToCBegin+macAlgoSToCLength]) + + compAlgoCToSBegin := 4 + macAlgoSToCBegin + macAlgoSToCLength + if length < 4+compAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + compAlgoCToSLength := int(big.NewInt(0).SetBytes(data[compAlgoCToSBegin : 4+compAlgoCToSBegin]).Uint64()) + if length < 4+compAlgoCToSBegin+compAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + compressionClientServer := string(data[4+compAlgoCToSBegin : 4+compAlgoCToSBegin+compAlgoCToSLength]) + + compAlgoSToCBegin := 4 + compAlgoCToSBegin + compAlgoCToSLength + if length < 4+compAlgoSToCBegin { + return nil, fmt.Errorf("invalid response length") + } + compAlgoSToCLength := int(big.NewInt(0).SetBytes(data[compAlgoSToCBegin : 4+compAlgoSToCBegin]).Uint64()) + if length < 4+compAlgoSToCBegin+compAlgoSToCLength { + return nil, fmt.Errorf("invalid response length") + } + compressionServerClient := string(data[4+compAlgoSToCBegin : 4+compAlgoSToCBegin+compAlgoSToCLength]) + + langAlgoCToSBegin := 4 + compAlgoSToCBegin + compAlgoSToCLength + if length < 4+langAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + langAlgoCToSLength := int(big.NewInt(0).SetBytes(data[langAlgoCToSBegin : 4+langAlgoCToSBegin]).Uint64()) + if length < 4+langAlgoCToSBegin+langAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + languagesClientServer := string(data[4+langAlgoCToSBegin : 4+langAlgoCToSBegin+langAlgoCToSLength]) + + langAlgoSToCBegin := 4 + langAlgoCToSBegin + langAlgoCToSLength + if length < 4+langAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + langAlgoSToCLength := int(big.NewInt(0).SetBytes(data[langAlgoSToCBegin : 4+langAlgoSToCBegin]).Uint64()) + if length < 4+langAlgoCToSBegin+langAlgoSToCLength { + return nil, fmt.Errorf("invalid response length") + } + languagesServerClient := string(data[4+langAlgoSToCBegin : 4+langAlgoSToCBegin+langAlgoSToCLength]) + + info := map[string]string{ + "Cookie": cookie, + "KexAlgos": kexAlgos, + "ServerHostKeyAlgos": serverHostKeyAlgos, + "CiphersClientServer": ciphersClientServer, + "CiphersServerClient": ciphersServerClient, + "MACsClientServer": macClientServer, + "MACsServerClient": macServerClient, + "CompressionClientServer": compressionClientServer, + "CompressionServerClient": compressionServerClient, + "LanguagesClientServer": languagesClientServer, + "LanguagesServerClient": languagesServerClient, + } + + return info, nil +} + +func (p *SSHPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + response, err := utils.Recv(conn, timeout) + passwordAuth := false + + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + banner, err := checkSSH(response) + if err != nil { + return nil, err + } + + msg := []byte("SSH-2.0-Rigour-SSH2\r\n") + + response, err = utils.SendRecv(conn, msg, timeout) + if err != nil { + return nil, err + } + + algo, err := checkAlgo(response) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + + // check auth methods + conf := ssh.ClientConfig{} + conf.Timeout = timeout + conf.Auth = nil + conf.Auth = append(conf.Auth, ssh.Password("admin")) + conf.Auth = append(conf.Auth, + ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { + answers := make([]string, len(questions)) + for i := range answers { + answers[i] = "password" + } + return answers, nil + }), + ) + + conf.User = "admin" + conf.HostKeyCallback = ssh.InsecureIgnoreHostKey() + // use all the ciphers supported by the go crypto ssh library + conf.KeyExchanges = append(conf.KeyExchanges, + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group1-sha1", + "diffie-hellman-group14-sha1", + "diffie-hellman-group14-sha256", + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + "curve25519-sha256@libssh.org", + "curve25519-sha256", + ) + conf.Ciphers = append(conf.Ciphers, + "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", + "chacha20-poly1305@openssh.com", + "arcfour256", "arcfour128", "arcfour", + "aes128-cbc", + "3des-cbc", + ) + + authClient, err := ssh.Dial("tcp", target.Address.String(), &conf) + + if err != nil { + passwordAuth = strings.Contains(err.Error(), "password") || strings.Contains(err.Error(), "keyboard-interactive") + } + + if authClient != nil { + authClient.Close() + } + + sshConfig := &ssh.ClientConfig{} + fullConf := *sshConfig + fullConf.SetDefaults() + + c := ssh.NewTransport(conn, fullConf.Rand, true) + t := ssh.NewHandshakeTransport(c, &fullConf.Config, msg, []byte(banner)) + sendMsg := ssh.KexInitMsg{ + KexAlgos: t.Config.KeyExchanges, + CiphersClientServer: t.Config.Ciphers, + CiphersServerClient: t.Config.Ciphers, + MACsClientServer: t.Config.MACs, + MACsServerClient: t.Config.MACs, + ServerHostKeyAlgos: ssh.SupportedHostKeyAlgos, + CompressionClientServer: []string{"none"}, + CompressionServerClient: []string{"none"}, + } + _, err = io.ReadFull(rand.Reader, sendMsg.Cookie[:]) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + if firstKeyExchange := t.SessionID == nil; firstKeyExchange { + sendMsg.KexAlgos = make([]string, 0, len(t.Config.KeyExchanges)+1) + sendMsg.KexAlgos = append(sendMsg.KexAlgos, t.Config.KeyExchanges...) + sendMsg.KexAlgos = append(sendMsg.KexAlgos, "ext-info-c") + } + packet := ssh.Marshal(sendMsg) + packetCopy := make([]byte, len(packet)) + copy(packetCopy, packet) + + err = ssh.PushPacket(t.HandshakeTransport, packetCopy) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + + cookie, err := hex.DecodeString(algo["cookie"]) + var ret [16]byte + copy(ret[:], cookie) + + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + otherInit := &ssh.KexInitMsg{ + KexAlgos: strings.Split(algo["KexAlgos"], ","), + Cookie: ret, + ServerHostKeyAlgos: strings.Split(algo["ServerHostKeyAlgos"], ","), + CiphersClientServer: strings.Split(algo["CiphersClientServer"], ","), + CiphersServerClient: strings.Split(algo["CiphersServerClient"], ","), + MACsClientServer: strings.Split(algo["MACsClientServer"], ","), + MACsServerClient: strings.Split(algo["MACsServerClient"], ","), + CompressionClientServer: strings.Split(algo["CompressionClientServer"], ","), + CompressionServerClient: strings.Split(algo["CompressionServerClient"], ","), + FirstKexFollows: false, + Reserved: 0, + } + + t.Algorithms, err = ssh.FindAgreedAlgorithms(false, &sendMsg, otherInit) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + magics := ssh.HandshakeMagics{ + ClientVersion: t.ClientVersion, + ServerVersion: t.ServerVersion, + ClientKexInit: packet, + ServerKexInit: response[5 : len(response)-10], + } + + kex := ssh.GetKex(t.Algorithms.Kex) + + result, err := ssh.Clients(t, kex, &magics) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + hostKey, err := ssh.ParsePublicKey(result.HostKey) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + fingerprint := ssh.FingerprintSHA256(hostKey) + base64HostKey := base64.StdEncoding.EncodeToString(result.HostKey) + + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + HostKey: base64HostKey, + HostKeyType: hostKey.Type(), + HostKeyFingerprint: fingerprint, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *SSHPlugin) Name() string { + return SSH +} + +func (p *SSHPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *SSHPlugin) Priority() int { + return 2 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/ssh/ssh_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/ssh/ssh_test.go new file mode 100644 index 0000000..fdab512 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/ssh/ssh_test.go @@ -0,0 +1,38 @@ +package ssh + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestSSH(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ssh", + Port: 22, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "sickp/alpine-sshd", + }, + }, + } + + p := &SSHPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/stun/stun.go b/rigour/pkg/crawler/fingerprint/plugins/services/stun/stun.go new file mode 100644 index 0000000..5546c1c --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/stun/stun.go @@ -0,0 +1,182 @@ +package stun + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "hash/crc32" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +const STUN = "stun" + +type Plugin struct{} + +var MessageHeaderLength = 20 +var FingerprintAttrLength = 8 +var BindingResponse = "0101" +var MagicCookie = "2112a442" +var ATTRIBUTES = map[uint32]string{ + 0x0001: "MappedAddress", + 0x0006: "Username", + 0x0008: "MessageIntegrity", + 0x0009: "ErrorCode", + 0x000a: "UnknownAttributes", + 0x0014: "Realm", + 0x0015: "Nonce", + 0x0020: "XORMappedAddress", + 0x8022: "Software", + 0x8023: "AlternateServer", + 0x8028: "Fingerprint", +} +var RigourXor uint32 = 0x5354554e + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + /** + * https://datatracker.ietf.org/doc/html/rfc8489 + * + * Sends binding request with FINGERPRINT attribute + * Checks if response contains valid message type, magic cookie, and transaction ID + */ + + InitialConnectionPackage := []byte{ + 0x00, 0x01, // Message Type (class: Request, method: Binding) + 0x00, 0x0c, // Message Length + 0x21, 0x12, 0xA4, 0x42, // Magic Cookie + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Transaction ID + + // Attribute: SOFTWARE + 0x80, 0x22, // attribute type + 0x0, 0x0, // attribute length + + // Attribute: FINGERPRINT + 0x80, 0x28, // attribute type + 0x0, 0x4, // attribute length + 0x0, 0x0, 0x0, 0x0, // CRC-32 checksum + } + _, err := rand.Read(InitialConnectionPackage[8:20]) // generate random transaction ID + if err != nil { + return nil, &utils.RandomizeError{Message: "transaction ID"} + } + TransactionID := hex.EncodeToString(InitialConnectionPackage[8:20]) + + fingerprintValue := crc32.ChecksumIEEE( + InitialConnectionPackage[:len(InitialConnectionPackage)-FingerprintAttrLength], + ) ^ RigourXor + for i := 1; i <= 4; i++ { + InitialConnectionPackage[len(InitialConnectionPackage)-i] = byte(fingerprintValue & 0xFF) + fingerprintValue >>= 8 + } + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + // check response + if len(response) < MessageHeaderLength { + return nil, nil + } + rmsgType, rmagicCookie, rtransID := hex.EncodeToString(response[:2]), + hex.EncodeToString(response[4:8]), + hex.EncodeToString(response[8:20]) + if rmsgType != BindingResponse { + return nil, nil + } + if rmagicCookie != MagicCookie { + return nil, nil + } + if rtransID != TransactionID { + return nil, nil + } + + // parse attributes (possibly optional) + infoMap, err := parseResponse(response) + if err != nil { + return nil, nil + } + payload := plugins.ServiceStun{ + Info: fmt.Sprintf("%s", infoMap), + } + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil +} + +func parseResponse(response []byte) (map[string]any, error) { + attrInfo := make(map[string]any) + idx := MessageHeaderLength + length := len(response) + for idx < length { + // parse attribute type, length + if idx+4 > length { + return nil, &utils.InvalidResponseErrorInfo{ + Service: "OpenVPN", + Info: "invalid attribute T/L header", + } + } + attrType, attrLen := (int(response[idx])<<8)+int(response[idx+1]), + (int(response[idx+2])<<8)+int(response[idx+3]) + idx += 4 + if attrLen == 0 { + continue + } + + // parse attribute value + if idx+attrLen > length { + return nil, &utils.InvalidResponseErrorInfo{ + Service: "OpenVPN", + Info: "invalid attribute length", + } + } + attrValue := response[idx : idx+attrLen] + idx += attrLen + var attrValueStr string + attrName, exists := ATTRIBUTES[uint32(attrType)] + if exists { + if attrName == "Software" { + attrValueStr = string(attrValue) + } else { + attrValueStr = hex.EncodeToString(attrValue) + } + } else { + attrName = fmt.Sprintf("%04x", attrType) + attrValueStr = hex.EncodeToString(attrValue) + } + + // update attribute info map + // if attribute appeared more than once, only process first occurence + _, exists = attrInfo[attrName] + if !exists { + attrInfo[attrName] = attrValueStr + } + } + + return attrInfo, nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 3478 +} + +func (p *Plugin) Name() string { + return STUN +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *Plugin) Priority() int { + return 2000 +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/stun/stun_test.go b/rigour/pkg/crawler/fingerprint/plugins/services/stun/stun_test.go new file mode 100644 index 0000000..5d54dfa --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/stun/stun_test.go @@ -0,0 +1,38 @@ +package stun + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/crawler/test" + "github.com/ory/dockertest/v3" +) + +func TestSTUN(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "stun", + Port: 3478, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "zenosmosis/docker-coturn", + ExposedPorts: []string{"3478/udp"}, + }, + }, + } + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Error(err) + } + }) + } +} diff --git a/rigour/pkg/crawler/fingerprint/plugins/services/telnet/telnet.go b/rigour/pkg/crawler/fingerprint/plugins/services/telnet/telnet.go new file mode 100644 index 0000000..45a6124 --- /dev/null +++ b/rigour/pkg/crawler/fingerprint/plugins/services/telnet/telnet.go @@ -0,0 +1,285 @@ +package telnet + +import ( + "encoding/hex" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/crawler/fingerprint/plugins/pluginutils" +) + +type TELNETPlugin struct{} + +const TELNET = "telnet" + +// https://www.rfc-editor.org/rfc/rfc854 +const IAC byte = 255 +const DONT byte = 254 +const DO byte = 253 +const WONT byte = 252 +const WILL byte = 251 +const SE byte = 240 +const NOP byte = 241 +const DM byte = 242 +const BRK byte = 243 +const IP byte = 244 +const AO byte = 245 +const AYT byte = 246 +const EC byte = 247 +const EL byte = 248 +const GA byte = 249 +const SB byte = 250 + +// https://users.cs.cf.ac.uk/Dave.Marshall/Internet/node141.html +const ECHO byte = 1 +const SUPPRESSGOAHEAD byte = 3 +const STATUS byte = 5 +const TIMINGMARK byte = 6 +const TERMTYPE byte = 24 +const WINDOWSIZE byte = 31 +const TERMSPEED byte = 32 +const REMOTEFLOWCTRL byte = 33 +const LINEMODE byte = 34 +const ENVVAR byte = 36 + +// https://www.iana.org/assignments/telnet-options/telnet-options.xhtml +// Binary Transmission [RFC856] +// Reconnection [NIC 15391 of 1973] +// Approx Message Size Negotiation [NIC 15393 of 1973] +// Remote Controlled Trans and Echo [RFC726] +// Output Line Width [NIC 20196 of August 1978] +// Output Page Size [NIC 20197 of August 1978] +// Output Carriage-Return Disposition [RFC652] +// Output Horizontal Tab Stops [RFC653] +// Output Horizontal Tab Disposition [RFC654] +// Output Formfeed Disposition [RFC655] +// Output Vertical Tabstops [RFC656] +// Output Vertical Tab Disposition [RFC657] +// Output Linefeed Disposition [RFC658] +// Extended ASCII [RFC698] +// Logout [RFC727] +// Byte Macro [RFC735] +// Data Entry Terminal [RFC1043][RFC732] +// SUPDUP [RFC736][RFC734] +// SUPDUP Output [RFC749] +// Send Location [RFC779] +// End of Record [RFC885] +// TACACS User Identification [RFC927] +// Output Marking [RFC933] +// Terminal Location Number [RFC946] +// Telnet 3270 Regime [RFC1041] +// X.3 PAD [RFC1053] +// X Display Location [RFC1096] +// Authentication Option [RFC2941] +// Encryption Option [RFC2946] +// New Environment Option [RFC1572] +// TN3270E [RFC2355] +// XAUTH [Rob_Earhart] +// CHARSET [RFC2066] +// Telnet Remote Serial Port (RSP) [Robert_Barnes] +// Com Port Control Option [RFC2217] +// Telnet Suppress Local Echo [Wirt_Atmar] +// Telnet Start TLS [Michael_Boe] +// KERMIT [RFC2840] +// SEND-URL [David_Croft] +// FORWARD_X [Jeffrey_Altman] +// TELOPT PRAGMA LOGON [Steve_McGregory] +// TELOPT SSPI LOGON [Steve_McGregory] +// TELOPT PRAGMA HEARTBEAT [Steve_McGregory] +const BinTransmission byte = 0 +const RECON byte = 2 +const ApproxMsgSizeNeg byte = 4 +const RemoteCtrlTE byte = 7 +const OUTLINEWIDTH byte = 8 +const OUTPAGESIZE byte = 9 +const OUTCRD byte = 10 +const OUTHTS byte = 11 +const OUTHTD byte = 12 +const OUTFFD byte = 13 +const OUTVT byte = 14 +const OUTVTD byte = 15 +const OUTLD byte = 16 +const EXTASCII byte = 17 +const LOGOUT byte = 18 +const BYTEMACRO byte = 19 +const DataEntryTerm byte = 20 +const SUPDUP byte = 21 +const SupdupOut byte = 22 +const SendLoc byte = 23 +const EOR byte = 25 +const TACAS byte = 26 +const OM byte = 27 +const TERMLOCN byte = 28 +const T3270 byte = 29 +const X3PAD byte = 30 +const XDISP byte = 35 +const AUTHOPT byte = 37 +const ENCOPT byte = 38 +const NEWENVOPT byte = 39 +const TN327 byte = 40 +const XAUTH byte = 41 +const CHARSET byte = 42 +const TRSP byte = 43 +const COMPORT byte = 44 +const TSLE byte = 45 +const TSTLS byte = 46 +const KERMIT byte = 47 +const SENDURL byte = 48 +const ForX byte = 49 +const TELPL byte = 138 +const TELSSPI byte = 139 +const TELPRAGMA byte = 140 + +var TelnetCommandMap = map[byte]bool{ + IAC: true, + DONT: true, + DO: true, + WONT: true, + WILL: true, + SE: true, + NOP: true, + DM: true, + BRK: true, + IP: true, + AO: true, + AYT: true, + EC: true, + EL: true, + GA: true, + SB: true, +} + +// https://users.cs.cf.ac.uk/Dave.Marshall/Internet/node141.html +// https://www.iana.org/assignments/telnet-options/telnet-options.xhtml +var TelnetOptionsMap = map[byte]bool{ + ECHO: true, + SUPPRESSGOAHEAD: true, + STATUS: true, + TIMINGMARK: true, + TERMTYPE: true, + WINDOWSIZE: true, + TERMSPEED: true, + REMOTEFLOWCTRL: true, + LINEMODE: true, + ENVVAR: true, + BinTransmission: true, + RECON: true, + ApproxMsgSizeNeg: true, + RemoteCtrlTE: true, + OUTLINEWIDTH: true, + OUTPAGESIZE: true, + OUTCRD: true, + OUTHTS: true, + OUTHTD: true, + OUTFFD: true, + OUTVT: true, + OUTVTD: true, + OUTLD: true, + EXTASCII: true, + LOGOUT: true, + BYTEMACRO: true, + DataEntryTerm: true, + SUPDUP: true, + SupdupOut: true, + SendLoc: true, + EOR: true, + TACAS: true, + OM: true, + TERMLOCN: true, + T3270: true, + X3PAD: true, + XDISP: true, + AUTHOPT: true, + ENCOPT: true, + NEWENVOPT: true, + TN327: true, + XAUTH: true, + CHARSET: true, + TRSP: true, + COMPORT: true, + TSLE: true, + TSTLS: true, + KERMIT: true, + SENDURL: true, + ForX: true, + TELPL: true, + TELSSPI: true, + TELPRAGMA: true, +} + +func isTelnet(telnet []byte) error { + msgLength := len(telnet) + matchError := &utils.InvalidResponseError{Service: TELNET} + + if msgLength == 0 || msgLength == 1 { + // a 0 or 1 byte response is probably not a telnet server + // matchError.Msg = "invalid message length" + return matchError + } + + // the first byte must be IAC + if telnet[0] != IAC { + // matchError.Msg = "missing IAC first byte" + return matchError + } + // if the next code isn't a valid telnet command probably not a telnet server + if _, ok := TelnetCommandMap[telnet[1]]; !ok { + // matchError.Msg = "invalid telnet command" + return matchError + } + + if msgLength == 2 { + // the first two bytes were valid telnet speak and only two bytes were sent, good chance this is a telnet server + return nil + } + + // msgLength is not 0, 1, or 2 (so it's 3 or greater) + // check if the 3rd byte is a valid telnet option, if it is this is likely a real telnet server sending: + // IAC,,