diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..2af77d60 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +# This file defines required reviewers for all PRs +# Last updated: Dec 2025 + +# Default reviewer(s) for everything +# For T3 2025 the list is primarily du-dhartley and romil-bijarnia. Any repository or organisation admins can also review and merge, even if not explicitly listed. +# Additional contributors can be easily added here. +* @du-dhartley @romil-bijarnia + +# You can add more specific rules later, e.g.: +# /docs/ @docs-reviewer \ No newline at end of file diff --git a/DevOps/short-test.yml b/.github/workflows/short-test.yml similarity index 100% rename from DevOps/short-test.yml rename to .github/workflows/short-test.yml diff --git a/.github/workflows/workflow-cleanup.yml b/.github/workflows/workflow-cleanup.yml new file mode 100644 index 00000000..ca59967b --- /dev/null +++ b/.github/workflows/workflow-cleanup.yml @@ -0,0 +1,66 @@ +name: "Cleanup Old Workflow Runs" + +on: + schedule: + - cron: "0 0 * * 0" # Every Sunday at midnight UTC + workflow_dispatch: + inputs: + dry_run: + description: "If true, only print what would be deleted" + required: true + type: boolean + default: true + confirm: + description: "Type DELETE to confirm deletion when dry_run=false" + required: false + default: "" + +permissions: + contents: read + actions: write + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install dependencies + working-directory: tools/workflow-cleanup + run: npm ci + + - name: Run cleanup script (scheduled dry run) + if: github.event_name == 'schedule' + working-directory: tools/workflow-cleanup + run: node cleanup-workflows.js --dryRun=true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.event.repository.name }} + + - name: Run cleanup script (manual) + if: github.event_name == 'workflow_dispatch' + working-directory: tools/workflow-cleanup + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.event.repository.name }} + run: | + if [ "${{ inputs.dry_run }}" = "true" ]; then + node cleanup-workflows.js --dryRun=true + exit 0 + fi + + if [ "${{ inputs.confirm }}" != "DELETE" ]; then + echo "Refusing to delete workflow runs." + echo "Re-run this workflow with dry_run=false and confirm=DELETE." + exit 1 + fi + + node cleanup-workflows.js \ No newline at end of file diff --git a/.gitignore b/.gitignore index f399cecf..4fd4fce2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,32 @@ env/ **/.venv/ **/venv/ **/env/ -README.md .DS_Store + +# Python caches +**/__pycache__/ +**/*.py[codz] +**/.pytest_cache/ +**/.ruff_cache/ +**/.mypy_cache/ + +# Node / frontend artifacts +**/node_modules/ +**/build/ +**/dist/ +**/coverage/ + +# Editor / OS +.idea/ +.vscode/ + +# Environment files (keep examples) +.env +.env.* +!.env.example +**/.env +**/.env.* +!**/.env.example + +# Engine legacy generated outputs +engine/legacy/engine/autoaudit_reports.json diff --git a/DevOps/cleanup.yml b/DevOps/cleanup.yml deleted file mode 100644 index c6341937..00000000 --- a/DevOps/cleanup.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: "Cleanup Old Workflows (Dry Run)" - -on: - schedule: - - cron: "0 0 * * 0" # Every Sunday at midnight UTC - workflow_dispatch: # Manual trigger from Actions tab - -jobs: - cleanup: - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 18 - - - name: Install dependencies - run: npm install - - - name: Run cleanup script (dry run) - run: npm run cleanup - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO_OWNER: ${{ github.repository_owner }} - REPO_NAME: ${{ github.event.repository.name }} - -# \ No newline at end of file diff --git a/README.md b/README.md index 33f5f7fa..d9fce5d7 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,20 @@ ## Project Overview AutoAudit is a M365 compliance automation platform built by several specialist teams. This monorepo centralizes all codebases—including backend services, APIs, compliance scanners, and frontends—enabling unified CI/CD, streamlined development, and rapid automated deployments to the cloud. +## Documentation + +- [Getting Started](docs/GETTING_STARTED.md) - Set up your development environment +- [Contributing Guide](docs/CONTRIBUTING.md) - Find where to contribute based on your skills + ## Repository Structure The repo follows the established modular structure: - `/backend-api` - `/security` - `/frontend` - `/engine` +- `/infrastructure` +- `/tools` +- `/docs` - `/.github/workflows` Full commit history and traceability from team forks are preserved. diff --git a/backend-api/.env.example b/backend-api/.env.example index 3a875157..c1fae044 100644 --- a/backend-api/.env.example +++ b/backend-api/.env.example @@ -1,14 +1,23 @@ -APP_ENV=dev -API_PREFIX=/api/v1 +# Local development settings +# Copy this file to .env and adjust as needed # Database DATABASE_URL=postgresql+asyncpg://autoaudit:autoaudit_dev_password@localhost:5432/autoaudit -# Authentication -# SECRET_KEY: Used for signing JWT tokens. MUST be changed in production. -# Generate a secure random key with one of these commands: -# python -c "import secrets; print(secrets.token_urlsafe(32))" -# openssl rand -hex 32 -SECRET_KEY=change-this-to-a-secure-random-string-in-production -ALGORITHM=HS256 -ACCESS_TOKEN_EXPIRE_MINUTES=30 +# Redis (Celery broker) +REDIS_URL=redis://localhost:6379 + +# OPA (Open Policy Agent) +OPA_URL=http://localhost:8181 + +# JWT signing key (MUST change in production) +SECRET_KEY=dev-secret-key-change-in-production + +# Fernet encryption key for M365 credentials at rest +# Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +# Must match docker-compose.yml ENCRYPTION_KEY for worker compatibility +# This is a randomly generated key and is not for production use +ENCRYPTION_KEY=Ps-HiS3ww5QzQPc_Mdu5-JyA_jCNbdFHMdiwWSlAfgM= + +# Policies directory (use relative path for local dev, /app/policies in Docker) +POLICIES_DIR=../engine/policies diff --git a/backend-api/Dockerfile b/backend-api/Dockerfile index b2eb1d8d..c06d9bd9 100644 --- a/backend-api/Dockerfile +++ b/backend-api/Dockerfile @@ -3,25 +3,30 @@ FROM python:3.11-slim WORKDIR /app -# Install system dependencies +# Install system dependencies (incl. Tesseract for evidence OCR) RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ + tesseract-ocr \ + libtesseract-dev \ && rm -rf /var/lib/apt/lists/* # Install uv RUN pip install uv # Copy dependency files -COPY pyproject.toml uv.lock ./ +COPY backend-api/pyproject.toml backend-api/uv.lock ./ -# Install dependencies -RUN uv sync --frozen --no-dev +# Install dependencies (include evidence extra so OCR/reporting stack is present) +RUN uv sync --frozen --no-dev --extra evidence # Copy application code -COPY app ./app -COPY alembic ./alembic -COPY alembic.ini ./ -COPY entrypoint.sh ./ +COPY backend-api/app ./app +COPY backend-api/alembic ./alembic +COPY backend-api/alembic.ini ./ +COPY backend-api/entrypoint.sh ./ + +# Copy evidence scanner assets/logic +COPY security ./security # Make entrypoint executable RUN chmod +x entrypoint.sh diff --git a/backend-api/alembic/env.py b/backend-api/alembic/env.py index 3e1ec40e..e7ab362f 100644 --- a/backend-api/alembic/env.py +++ b/backend-api/alembic/env.py @@ -10,7 +10,15 @@ # Import models for autogenerate support from app.db.base import Base from app.models.user import User # noqa -from app.models.compliance import Tenant, Rule, Scan, Issue # noqa +from app.models.oauth_account import OAuthAccount # noqa +from app.models.m365_connection import M365Connection # noqa +from app.models.platform import Platform # noqa +from app.models.compliance import Scan # noqa +from app.models.scan_result import ScanResult # noqa +from app.models.evidence_validation import EvidenceValidation # noqa +from app.models.azure_connection import AzureConnection # noqa +from app.models.gcp_connection import GCPConnection # noqa +from app.models.aws_connection import AWSConnection # noqa from app.core.config import get_settings # this is the Alembic Config object, which provides diff --git a/backend-api/alembic/versions/c3d4e5f6g789_add_m365_connection_update_scan.py b/backend-api/alembic/versions/c3d4e5f6g789_add_m365_connection_update_scan.py new file mode 100644 index 00000000..3baf578e --- /dev/null +++ b/backend-api/alembic/versions/c3d4e5f6g789_add_m365_connection_update_scan.py @@ -0,0 +1,165 @@ +"""Add m365_connection table and update scan table + +Revision ID: c3d4e5f6g789 +Revises: b1c2d3e4f567 +Create Date: 2024-12-09 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision: str = "c3d4e5f6g789" +down_revision: Union[str, Sequence[str], None] = "b1c2d3e4f567" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Add m365_connection table, update scan table, drop tenant table.""" + + # Create m365_connection table + op.create_table( + "m365_connection", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("tenant_id", sa.String(length=255), nullable=False), + sa.Column("client_id", sa.String(length=255), nullable=False), + sa.Column("encrypted_client_secret", sa.Text(), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.Column( + "updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_m365_connection_user_id"), + "m365_connection", + ["user_id"], + unique=False, + ) + + # Update scan table: add new columns + op.add_column( + "scan", + sa.Column("m365_connection_id", sa.Integer(), nullable=True), + ) + op.add_column( + "scan", + sa.Column("framework", sa.String(length=50), nullable=True), + ) + op.add_column( + "scan", + sa.Column("benchmark", sa.String(length=100), nullable=True), + ) + op.add_column( + "scan", + sa.Column("version", sa.String(length=20), nullable=True), + ) + op.add_column( + "scan", + sa.Column( + "control_ids", postgresql.JSONB(astext_type=sa.Text()), nullable=True + ), + ) + + # Add control_id to issue table + op.add_column( + "issue", + sa.Column("control_id", sa.String(length=50), nullable=True), + ) + + # Create foreign key for m365_connection_id + op.create_foreign_key( + "fk_scan_m365_connection_id", + "scan", + "m365_connection", + ["m365_connection_id"], + ["id"], + ) + op.create_index( + op.f("ix_scan_m365_connection_id"), + "scan", + ["m365_connection_id"], + unique=False, + ) + + # Drop tenant_id FK and column from scan + op.drop_index(op.f("ix_scan_tenant_id"), table_name="scan") + op.drop_constraint("scan_tenant_id_fkey", "scan", type_="foreignkey") + op.drop_column("scan", "tenant_id") + + # Drop tenant table + op.drop_table("tenant") + + # Make framework/benchmark/version NOT NULL for new scans + # (existing rows need to be handled - setting defaults first) + op.execute("UPDATE scan SET framework = 'unknown' WHERE framework IS NULL") + op.execute("UPDATE scan SET benchmark = 'unknown' WHERE benchmark IS NULL") + op.execute("UPDATE scan SET version = 'unknown' WHERE version IS NULL") + + op.alter_column("scan", "framework", nullable=False) + op.alter_column("scan", "benchmark", nullable=False) + op.alter_column("scan", "version", nullable=False) + + +def downgrade() -> None: + """Reverse the migration.""" + + # Make framework/benchmark/version nullable again + op.alter_column("scan", "version", nullable=True) + op.alter_column("scan", "benchmark", nullable=True) + op.alter_column("scan", "framework", nullable=True) + + # Recreate tenant table + op.create_table( + "tenant", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("external_tenant_id", sa.String(length=255), nullable=True), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.PrimaryKeyConstraint("id"), + ) + + # Add tenant_id back to scan + op.add_column( + "scan", + sa.Column("tenant_id", sa.Integer(), nullable=True), + ) + op.create_foreign_key( + "scan_tenant_id_fkey", "scan", "tenant", ["tenant_id"], ["id"] + ) + op.create_index(op.f("ix_scan_tenant_id"), "scan", ["tenant_id"], unique=False) + + # Drop m365_connection references from scan + op.drop_index(op.f("ix_scan_m365_connection_id"), table_name="scan") + op.drop_constraint("fk_scan_m365_connection_id", "scan", type_="foreignkey") + + # Drop new columns from issue + op.drop_column("issue", "control_id") + + # Drop new columns from scan + op.drop_column("scan", "control_ids") + op.drop_column("scan", "version") + op.drop_column("scan", "benchmark") + op.drop_column("scan", "framework") + op.drop_column("scan", "m365_connection_id") + + # Drop m365_connection table + op.drop_index(op.f("ix_m365_connection_user_id"), table_name="m365_connection") + op.drop_table("m365_connection") diff --git a/backend-api/alembic/versions/d4e5f6g7h890_database_architecture_revision.py b/backend-api/alembic/versions/d4e5f6g7h890_database_architecture_revision.py new file mode 100644 index 00000000..5dcfaf40 --- /dev/null +++ b/backend-api/alembic/versions/d4e5f6g7h890_database_architecture_revision.py @@ -0,0 +1,238 @@ +"""Database architecture revision + +- Create platform lookup table with seed data +- Create stub connection tables (azure, gcp, aws) +- Create scan_result table (replaces issue) +- Update scan table: add user_id FK, connection FKs, drop control_ids, add skipped_count, rename not_tested_count +- Drop issue and rule tables + +Revision ID: d4e5f6g7h890 +Revises: c3d4e5f6g789 +Create Date: 2024-12-10 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision: str = "d4e5f6g7h890" +down_revision: Union[str, Sequence[str], None] = "c3d4e5f6g789" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Apply database architecture revision.""" + + # 1. Create platform lookup table + op.create_table( + "platform", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=50), nullable=False), + sa.Column("display_name", sa.String(length=100), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="false"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), + ) + + # Seed platform data - only M365 is active + op.execute(""" + INSERT INTO platform (name, display_name, is_active) VALUES + ('m365', 'Microsoft 365', true), + ('azure', 'Microsoft Azure', false), + ('gcp', 'Google Cloud Platform', false), + ('aws', 'Amazon Web Services', false) + """) + + # 2. Create stub connection tables (id only - columns added when implemented) + op.create_table( + "azure_connection", + sa.Column("id", sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_table( + "gcp_connection", + sa.Column("id", sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_table( + "aws_connection", + sa.Column("id", sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + # 3. Create scan_result table (replaces issue) + op.create_table( + "scan_result", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("scan_id", sa.Integer(), nullable=False), + sa.Column("control_id", sa.String(length=50), nullable=False), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("message", sa.Text(), nullable=True), + sa.Column("evidence", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.Column( + "updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.ForeignKeyConstraint(["scan_id"], ["scan.id"]), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("scan_id", "control_id", name="uq_scan_result_scan_control"), + ) + op.create_index( + op.f("ix_scan_result_scan_id"), "scan_result", ["scan_id"], unique=False + ) + + # 4. Update scan table + # Add user_id FK (NOT NULL - starting fresh, but we need to handle existing rows) + op.add_column("scan", sa.Column("user_id", sa.Integer(), nullable=True)) + + # For any existing scans, assign them to user 1 (admin) if exists + # In practice, we're starting fresh so this shouldn't matter + op.execute(""" + UPDATE scan SET user_id = (SELECT id FROM "user" LIMIT 1) + WHERE user_id IS NULL + """) + + # Now make user_id NOT NULL + op.alter_column("scan", "user_id", nullable=False) + + op.create_foreign_key( + "fk_scan_user_id", "scan", "user", ["user_id"], ["id"] + ) + op.create_index(op.f("ix_scan_user_id"), "scan", ["user_id"], unique=False) + + # Add connection FKs with actual foreign key constraints + op.add_column( + "scan", sa.Column("azure_connection_id", sa.Integer(), nullable=True) + ) + op.create_foreign_key( + "fk_scan_azure_connection_id", + "scan", + "azure_connection", + ["azure_connection_id"], + ["id"], + ) + + op.add_column( + "scan", sa.Column("gcp_connection_id", sa.Integer(), nullable=True) + ) + op.create_foreign_key( + "fk_scan_gcp_connection_id", + "scan", + "gcp_connection", + ["gcp_connection_id"], + ["id"], + ) + + op.add_column( + "scan", sa.Column("aws_connection_id", sa.Integer(), nullable=True) + ) + op.create_foreign_key( + "fk_scan_aws_connection_id", + "scan", + "aws_connection", + ["aws_connection_id"], + ["id"], + ) + + # Remove control_ids - ScanResult records track selection + op.drop_column("scan", "control_ids") + + # Rename not_tested_count to error_count + op.alter_column("scan", "not_tested_count", new_column_name="error_count") + + # Add skipped_count + op.add_column( + "scan", + sa.Column("skipped_count", sa.Integer(), nullable=False, server_default="0"), + ) + + # 5. Drop issue table (replaced by scan_result) + op.drop_index(op.f("ix_issue_scan_id"), table_name="issue") + op.drop_table("issue") + + # 6. Drop rule table (unused) + op.drop_table("rule") + + +def downgrade() -> None: + """Reverse the database architecture revision.""" + + # 1. Recreate rule table + op.create_table( + "rule", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("framework", sa.String(length=100), nullable=False), + sa.Column("control_key", sa.String(length=100), nullable=False), + sa.Column("title", sa.Text(), nullable=False), + sa.Column("severity", sa.String(length=20), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + # 2. Recreate issue table + op.create_table( + "issue", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("scan_id", sa.Integer(), nullable=False), + sa.Column("rule_id", sa.Integer(), nullable=True), + sa.Column("control_id", sa.String(length=50), nullable=True), + sa.Column("priority", sa.String(length=20), nullable=True), + sa.Column("title", sa.Text(), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("result", sa.String(length=20), nullable=True), + sa.Column("evidence", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.ForeignKeyConstraint(["rule_id"], ["rule.id"]), + sa.ForeignKeyConstraint(["scan_id"], ["scan.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_issue_scan_id"), "issue", ["scan_id"], unique=False) + + # 3. Revert scan table changes + op.drop_column("scan", "skipped_count") + op.alter_column("scan", "error_count", new_column_name="not_tested_count") + op.add_column( + "scan", + sa.Column( + "control_ids", postgresql.JSONB(astext_type=sa.Text()), nullable=True + ), + ) + + # Drop connection FKs + op.drop_constraint("fk_scan_aws_connection_id", "scan", type_="foreignkey") + op.drop_column("scan", "aws_connection_id") + + op.drop_constraint("fk_scan_gcp_connection_id", "scan", type_="foreignkey") + op.drop_column("scan", "gcp_connection_id") + + op.drop_constraint("fk_scan_azure_connection_id", "scan", type_="foreignkey") + op.drop_column("scan", "azure_connection_id") + + # Drop user_id + op.drop_index(op.f("ix_scan_user_id"), table_name="scan") + op.drop_constraint("fk_scan_user_id", "scan", type_="foreignkey") + op.drop_column("scan", "user_id") + + # 4. Drop scan_result table + op.drop_index(op.f("ix_scan_result_scan_id"), table_name="scan_result") + op.drop_table("scan_result") + + # 5. Drop stub connection tables + op.drop_table("aws_connection") + op.drop_table("gcp_connection") + op.drop_table("azure_connection") + + # 6. Drop platform table + op.drop_table("platform") diff --git a/backend-api/alembic/versions/f6c7d8e9f012_add_evidence_validation_table.py b/backend-api/alembic/versions/f6c7d8e9f012_add_evidence_validation_table.py new file mode 100644 index 00000000..e73df4e3 --- /dev/null +++ b/backend-api/alembic/versions/f6c7d8e9f012_add_evidence_validation_table.py @@ -0,0 +1,77 @@ +"""Add evidence validation table (int PK + user FK). + +Revision ID: f6c7d8e9f012 +Revises: d4e5f6g7h890 +Create Date: 2025-12-21 +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision: str = "f6c7d8e9f012" +down_revision: Union[str, Sequence[str], None] = "d4e5f6g7h890" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + op.create_table( + "evidence_validation", + sa.Column("id", sa.Integer(), sa.Identity(), nullable=False), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("strategy_name", sa.String(length=255), nullable=False), + sa.Column("source_filename", sa.String(length=512), nullable=True), + sa.Column("text_hash", sa.String(length=64), nullable=True), + sa.Column("extracted_text_encrypted", sa.Text(), nullable=True), + sa.Column("matches_json", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column( + "status", sa.String(length=25), server_default="success", nullable=False + ), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_foreign_key( + "fk_evidence_validation_user_id", + "evidence_validation", + "user", + ["user_id"], + ["id"], + ) + + op.create_index( + op.f("ix_evidence_validation_created_at"), + "evidence_validation", + ["created_at"], + unique=False, + ) + op.create_index( + op.f("ix_evidence_validation_user_id"), + "evidence_validation", + ["user_id"], + unique=False, + ) + + +def downgrade() -> None: + """Downgrade schema.""" + op.drop_index(op.f("ix_evidence_validation_user_id"), table_name="evidence_validation") + op.drop_index( + op.f("ix_evidence_validation_created_at"), table_name="evidence_validation" + ) + op.drop_constraint( + "fk_evidence_validation_user_id", + "evidence_validation", + type_="foreignkey", + ) + op.drop_table("evidence_validation") + + diff --git a/backend-api/alembic/versions/h1i2j3k4l567_expand_oauth_account_token_columns.py b/backend-api/alembic/versions/h1i2j3k4l567_expand_oauth_account_token_columns.py new file mode 100644 index 00000000..35b03065 --- /dev/null +++ b/backend-api/alembic/versions/h1i2j3k4l567_expand_oauth_account_token_columns.py @@ -0,0 +1,116 @@ +"""Add oauth_account table (squashed). + +This migration squashes the original two-step history: +- g7h8i9j0k123: create oauth_account table +- h1i2j3k4l567: expand token columns (access_token/refresh_token) to TEXT + +We create the table in its final schema (token columns as TEXT) to avoid +needing a follow-up migration. + +Revision ID: h1i2j3k4l567 +Revises: f6c7d8e9f012 +Create Date: 2025-12-27 +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "h1i2j3k4l567" +down_revision: Union[str, Sequence[str], None] = "f6c7d8e9f012" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None +# Support databases that may already be stamped at the original create-table revision. +replaces: Union[str, Sequence[str], None] = ("g7h8i9j0k123",) + + +def upgrade() -> None: + """Upgrade schema.""" + bind = op.get_bind() + inspector = sa.inspect(bind) + + existing_tables = set(inspector.get_table_names()) + if "oauth_account" not in existing_tables: + op.create_table( + "oauth_account", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("oauth_name", sa.String(length=100), nullable=False), + # Use TEXT for provider tokens (can exceed 1024 chars). + sa.Column("access_token", sa.Text(), nullable=False), + sa.Column("expires_at", sa.Integer(), nullable=True), + sa.Column("refresh_token", sa.Text(), nullable=True), + sa.Column("account_id", sa.String(length=320), nullable=False), + sa.Column("account_email", sa.String(length=320), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "oauth_name", + "account_id", + name="uq_oauth_account_oauth_name_account_id", + ), + ) + + op.create_index( + op.f("ix_oauth_account_oauth_name"), + "oauth_account", + ["oauth_name"], + ) + op.create_index( + op.f("ix_oauth_account_account_id"), + "oauth_account", + ["account_id"], + ) + op.create_index( + op.f("ix_oauth_account_user_id"), + "oauth_account", + ["user_id"], + ) + return + + # If the table already exists (e.g., database was created with the old migration), + # ensure token columns are large enough. + op.alter_column( + "oauth_account", + "access_token", + existing_type=sa.String(length=1024), + type_=sa.Text(), + existing_nullable=False, + ) + op.alter_column( + "oauth_account", + "refresh_token", + existing_type=sa.String(length=1024), + type_=sa.Text(), + existing_nullable=True, + ) + + +def downgrade() -> None: + """Downgrade schema.""" + bind = op.get_bind() + inspector = sa.inspect(bind) + + if "oauth_account" not in set(inspector.get_table_names()): + return + + # Drop indexes first (naming convention uses op.f()). + op.drop_index(op.f("ix_oauth_account_user_id"), table_name="oauth_account") + op.drop_index(op.f("ix_oauth_account_account_id"), table_name="oauth_account") + op.drop_index(op.f("ix_oauth_account_oauth_name"), table_name="oauth_account") + op.drop_table("oauth_account") + + + + + + + + + + + + + diff --git a/backend-api/app/api/v1/audit.py b/backend-api/app/api/v1/audit.py deleted file mode 100644 index 73a320b8..00000000 --- a/backend-api/app/api/v1/audit.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import APIRouter, Query -from app.schemas.audit import AuditLog -from app.core.errors import NotFound - -router = APIRouter(prefix="/audit", tags=["Audit"]) - -# Mock audit log store -logs = [ - {"timestamp": "2025-09-18T02:00:00Z", "action": "scan_started", "resource": "scan", "id": "s-001"}, - {"timestamp": "2025-09-18T02:01:00Z", "action": "scan_completed", "resource": "scan", "id": "s-001"} -] - -@router.get("/logs", response_model=list[AuditLog]) -def get_audit_logs(resource: str, id: str, limit: int = Query(100, le=500)): - filtered = [log for log in logs if log["resource"] == resource and log["id"] == id] - - if not filtered: - raise NotFound(f"Audit logs for resource={resource}, id={id}") - - return filtered[:limit] diff --git a/backend-api/app/api/v1/auth.py b/backend-api/app/api/v1/auth.py index f4edf784..fa39f0d5 100644 --- a/backend-api/app/api/v1/auth.py +++ b/backend-api/app/api/v1/auth.py @@ -1,12 +1,26 @@ -from fastapi import APIRouter, Depends +import secrets +from urllib.parse import urlencode + +import httpx +from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi_users import exceptions -from app.core.users import fastapi_users, auth_backend -from app.schemas.user import UserRead, UserCreate, UserUpdate +from fastapi.responses import RedirectResponse +from httpx_oauth.clients.google import GoogleOAuth2 + +from app.core.config import get_settings +from app.core.users import auth_backend, fastapi_users, get_jwt_strategy, get_user_manager +from app.schemas.user import UserRead, UserCreate, UserRegister, UserUpdate from app.core.auth import get_current_user from app.models.user import User router = APIRouter(prefix="/auth", tags=["Authentication"]) +# Registration endpoint (creates users in DB; stores hashed_password) +router.include_router( + fastapi_users.get_register_router(UserRead, UserRegister), + prefix="", +) + # Login endpoint router.include_router( fastapi_users.get_auth_router(auth_backend), @@ -70,3 +84,206 @@ async def change_password( # Include users router router.include_router(users_router) + + +GOOGLE_OAUTH_STATE_COOKIE = "google_oauth_state" + + +def _google_redirect_uri() -> str: + settings = get_settings() + return f"{settings.BACKEND_PUBLIC_URL}{settings.API_PREFIX}/auth/google/callback" + + +def _frontend_google_callback_url(fragment_params: dict[str, str]) -> str: + settings = get_settings() + base = settings.FRONTEND_URL.rstrip("/") + fragment = urlencode(fragment_params) + return f"{base}/auth/google/callback#{fragment}" + + +def _google_oauth_client() -> GoogleOAuth2: + settings = get_settings() + if not settings.GOOGLE_OAUTH_CLIENT_ID or not settings.GOOGLE_OAUTH_CLIENT_SECRET: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Google OAuth is not configured (missing GOOGLE_OAUTH_CLIENT_ID/SECRET).", + ) + return GoogleOAuth2( + settings.GOOGLE_OAUTH_CLIENT_ID, + settings.GOOGLE_OAUTH_CLIENT_SECRET, + scopes=["openid", "email", "profile"], + name="google", + ) + + +@router.get("/google/authorize") +async def google_authorize() -> RedirectResponse: + """Start Google OAuth (redirect to Google authorize URL).""" + settings = get_settings() + state = secrets.token_urlsafe(32) + + try: + client = _google_oauth_client() + except HTTPException as exc: + return RedirectResponse( + _frontend_google_callback_url( + { + "error": "oauth_not_configured", + "error_description": str(exc.detail), + } + ), + status_code=status.HTTP_302_FOUND, + ) + + authorize_url = await client.get_authorization_url( + redirect_uri=_google_redirect_uri(), + state=state, + scope=["openid", "email", "profile"], + # Don't force prompt=select_account; it adds an extra step and slows SSO. + extras_params=None, + ) + + response = RedirectResponse(authorize_url, status_code=status.HTTP_302_FOUND) + response.set_cookie( + key=GOOGLE_OAUTH_STATE_COOKIE, + value=state, + max_age=600, + httponly=True, + secure=settings.BACKEND_PUBLIC_URL.startswith("https://"), + samesite="lax", + path=f"{settings.API_PREFIX}/auth/google/callback", + ) + return response + + +@router.get("/google/callback") +async def google_callback( + request: Request, + code: str | None = None, + state: str | None = None, + user_manager=Depends(get_user_manager), +) -> RedirectResponse: + """Google OAuth callback: exchange code, link/create user, mint AutoAudit JWT, redirect to FE.""" + settings = get_settings() + + cookie_state = request.cookies.get(GOOGLE_OAUTH_STATE_COOKIE) + if not state or not cookie_state or state != cookie_state: + return RedirectResponse( + _frontend_google_callback_url( + { + "error": "invalid_state", + "error_description": "Invalid OAuth state. Please try again.", + } + ), + status_code=status.HTTP_302_FOUND, + ) + + if not code: + return RedirectResponse( + _frontend_google_callback_url( + { + "error": "missing_code", + "error_description": "Google did not return an authorization code.", + } + ), + status_code=status.HTTP_302_FOUND, + ) + + client = _google_oauth_client() + + try: + token = await client.get_access_token(code, redirect_uri=_google_redirect_uri()) + google_access_token = token["access_token"] + except Exception: + return RedirectResponse( + _frontend_google_callback_url( + { + "error": "token_exchange_failed", + "error_description": "Failed to exchange authorization code for tokens.", + } + ), + status_code=status.HTTP_302_FOUND, + ) + + # Fetch OIDC userinfo for email + verification + stable subject identifier (sub). + try: + async with httpx.AsyncClient(timeout=10.0) as http: + resp = await http.get( + "https://openidconnect.googleapis.com/v1/userinfo", + headers={"Authorization": f"Bearer {google_access_token}"}, + ) + resp.raise_for_status() + profile = resp.json() + except Exception: + return RedirectResponse( + _frontend_google_callback_url( + { + "error": "userinfo_failed", + "error_description": "Failed to fetch Google user profile.", + } + ), + status_code=status.HTTP_302_FOUND, + ) + + email = profile.get("email") + email_verified = profile.get("email_verified") + sub = profile.get("sub") + + if not email or not sub: + return RedirectResponse( + _frontend_google_callback_url( + { + "error": "invalid_profile", + "error_description": "Google profile is missing required fields.", + } + ), + status_code=status.HTTP_302_FOUND, + ) + + # Link-by-email requires the email to be verified to avoid account takeover. + if email_verified is not True: + return RedirectResponse( + _frontend_google_callback_url( + { + "error": "email_not_verified", + "error_description": "Google account email is not verified.", + } + ), + status_code=status.HTTP_302_FOUND, + ) + + try: + user = await user_manager.oauth_callback( + oauth_name="google", + access_token=google_access_token, + account_id=sub, + account_email=email, + expires_at=token.get("expires_at"), + refresh_token=token.get("refresh_token"), + request=request, + associate_by_email=True, + is_verified_by_default=True, + ) + except Exception: + return RedirectResponse( + _frontend_google_callback_url( + { + "error": "user_link_failed", + "error_description": "Failed to link Google account to user.", + } + ), + status_code=status.HTTP_302_FOUND, + ) + + # fastapi-users JWTStrategy.write_token is async in the version used by the backend container. + autoaudit_token = await get_jwt_strategy().write_token(user) + redirect_url = _frontend_google_callback_url( + {"access_token": autoaudit_token, "token_type": "bearer"} + ) + + response = RedirectResponse(redirect_url, status_code=status.HTTP_302_FOUND) + response.delete_cookie( + GOOGLE_OAUTH_STATE_COOKIE, + path=f"{settings.API_PREFIX}/auth/google/callback", + ) + return response diff --git a/backend-api/app/api/v1/benchmarks.py b/backend-api/app/api/v1/benchmarks.py new file mode 100644 index 00000000..375bb2c5 --- /dev/null +++ b/backend-api/app/api/v1/benchmarks.py @@ -0,0 +1,159 @@ +"""Benchmark and control discovery API endpoints. + +These endpoints read from metadata.json files in the policies directory +to allow the frontend to discover available benchmarks and controls. +""" + +from fastapi import APIRouter, Depends, HTTPException, status + +from app.core.auth import get_current_user +from app.models.user import User +from app.schemas.benchmark import BenchmarkRead, ControlRead +from app.services.benchmark_reader import get_file_reader + +router = APIRouter(prefix="/benchmarks", tags=["Benchmarks"]) + + +@router.get("", response_model=list[BenchmarkRead]) +async def list_benchmarks( + current_user: User = Depends(get_current_user), +) -> list[BenchmarkRead]: + """List all available benchmarks. + + Scans the policies directory for metadata.json files and returns + information about each available benchmark. + """ + file_reader = get_file_reader() + benchmarks_data = file_reader.list_benchmarks() + + result = [] + for data in benchmarks_data: + controls = data.get("controls", []) + result.append( + BenchmarkRead( + framework=data.get("framework", ""), + slug=data.get("slug", ""), + version=data.get("version", ""), + name=data.get("benchmark", ""), + platform=data.get("platform", ""), + release_date=data.get("release_date"), + source_url=data.get("source_url"), + control_count=len(controls), + ) + ) + return result + + +@router.get("/{framework}/{slug}/{version}", response_model=BenchmarkRead) +async def get_benchmark( + framework: str, + slug: str, + version: str, + current_user: User = Depends(get_current_user), +) -> BenchmarkRead: + """Get details for a specific benchmark.""" + file_reader = get_file_reader() + + try: + data = file_reader.get_benchmark_metadata(framework, slug, version) + except FileNotFoundError: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Benchmark {framework}/{slug}/{version} not found", + ) + + controls = data.get("controls", []) + return BenchmarkRead( + framework=data.get("framework", framework), + slug=data.get("slug", slug), + version=data.get("version", version), + name=data.get("benchmark", ""), + platform=data.get("platform", ""), + release_date=data.get("release_date"), + source_url=data.get("source_url"), + control_count=len(controls), + ) + + +@router.get("/{framework}/{slug}/{version}/controls", response_model=list[ControlRead]) +async def list_controls( + framework: str, + slug: str, + version: str, + current_user: User = Depends(get_current_user), +) -> list[ControlRead]: + """List all controls for a specific benchmark.""" + file_reader = get_file_reader() + + try: + controls_data = file_reader.list_controls(framework, slug, version) + except FileNotFoundError: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Benchmark {framework}/{slug}/{version} not found", + ) + + result = [] + for control in controls_data: + result.append( + ControlRead( + control_id=control.get("control_id", ""), + title=control.get("title", ""), + description=control.get("description"), + severity=control.get("severity"), + service=control.get("service"), + level=control.get("level", ""), + is_manual=control.get("is_manual", False), + benchmark_audit_type=control.get("benchmark_audit_type", ""), + automation_status=control.get("automation_status", "not_started"), + data_collector_id=control.get("data_collector_id"), + policy_file=control.get("policy_file"), + requires_permissions=control.get("requires_permissions"), + notes=control.get("notes"), + ) + ) + return result + + +@router.get( + "/{framework}/{slug}/{version}/controls/{control_id}", + response_model=ControlRead, +) +async def get_control( + framework: str, + slug: str, + version: str, + control_id: str, + current_user: User = Depends(get_current_user), +) -> ControlRead: + """Get details for a specific control.""" + file_reader = get_file_reader() + + try: + control = file_reader.get_control_metadata(framework, slug, version, control_id) + except FileNotFoundError: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Benchmark {framework}/{slug}/{version} not found", + ) + except ValueError: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Control {control_id} not found in {framework}/{slug}/{version}", + ) + + return ControlRead( + control_id=control.get("control_id", ""), + title=control.get("title", ""), + description=control.get("description"), + severity=control.get("severity"), + service=control.get("service"), + level=control.get("level", ""), + is_manual=control.get("is_manual", False), + benchmark_audit_type=control.get("benchmark_audit_type", ""), + automation_status=control.get("automation_status", "not_started"), + data_collector_id=control.get("data_collector_id"), + policy_file=control.get("policy_file"), + requires_permissions=control.get("requires_permissions"), + notes=control.get("notes"), + ) diff --git a/backend-api/app/api/v1/evidence.py b/backend-api/app/api/v1/evidence.py new file mode 100644 index 00000000..5e0202eb --- /dev/null +++ b/backend-api/app/api/v1/evidence.py @@ -0,0 +1,196 @@ +import hashlib +import json + +from fastapi import APIRouter, Depends, UploadFile, File, Form +from fastapi.responses import JSONResponse, RedirectResponse, FileResponse +from sqlalchemy.ext.asyncio import AsyncSession + +# Ensure the monorepo /security package is importable both locally and inside Docker +import sys +from pathlib import Path + + +def _find_security_dir() -> Path | None: + here = Path(__file__).resolve() + for ancestor in here.parents: + candidate = ancestor / "security" + if candidate.exists(): + return candidate + return None + + +SECURITY_DIR = _find_security_dir() +if SECURITY_DIR and str(SECURITY_DIR.parent) not in sys.path: + sys.path.insert(0, str(SECURITY_DIR.parent)) + +# Reuse existing evidence logic from security package +from security.evidence_ui import app as evidence_ui + +from app.core.auth import get_current_user +from app.db.session import get_async_session +from app.models.evidence_validation import EvidenceValidation +from app.models.user import User +from app.services.encryption import encrypt +from app.services.evidence_validator import validate_text + +router = APIRouter(prefix="/evidence", tags=["evidence"]) + + +@router.get("/strategies") +async def strategies(): + """ + Backend API: list available evidence strategies for the frontend dropdown. + + Used by frontend/src/pages/Evidence.js via: + - frontend/src/api/client.js -> getEvidenceStrategies() + - GET /v1/evidence/strategies + """ + # Delegate to the existing evidence UI module (security/evidence_ui/app.py). + return evidence_ui.api_strategies() + + +@router.get("/health") +async def health(): + """Backend API: health/debug endpoint for evidence scanning stack.""" + return evidence_ui.health() + + +@router.get("/scan-mem") +async def scan_mem(): + """Serve the human-friendly recent scans page (HTML).""" + return evidence_ui.scan_mem_page() + + +@router.get("/scan-mem-log") +async def scan_mem_log(): + """Return recent scans as JSON for the frontend.""" + return evidence_ui.api_get_scan_mem_log() + + +@router.get("/recent-scans", include_in_schema=False) +async def recent_scans_redirect(): + return RedirectResponse(url="/v1/evidence/scan-mem") + + +@router.get("/scan-log", include_in_schema=False) +async def scan_log_redirect(): + return RedirectResponse(url="/v1/evidence/scan-mem") + + +@router.post("/scan") +async def scan( + evidence: UploadFile = File(...), + strategy_name: str = Form(...), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Backend API: run an evidence scan. + + Used by frontend/src/pages/Evidence.js via: + - frontend/src/api/client.js -> scanEvidence() + - POST /v1/evidence/scan (multipart/form-data) + + Responsibilities in this layer: + - (Best-effort) extract text + run validator pre-pass + - Delegate the actual scanning to security/evidence_ui/app.py + - (Best-effort) store validator output in DB (evidence_validation table) + - Return the original scan response shape so the frontend can render it + """ + # --- Validator pre-pass (best-effort) --- + extracted_text = "" + validator_payload: dict | None = None + text_hash: str | None = None + extracted_text_encrypted: str | None = None + + try: + content_bytes = await evidence.read() + await evidence.seek(0) + + extracted_text, _preview_path = evidence_ui.extract_text_and_preview_bytes( + evidence.filename or "", content_bytes, evidence_ui.PREVIEWS + ) + validator_payload = validate_text(strategy_name, extracted_text) + + if extracted_text: + text_hash = hashlib.sha256(extracted_text.encode("utf-8", errors="ignore")).hexdigest() + except Exception: + # Do not block scan if validator pre-pass fails. + extracted_text = "" + validator_payload = None + text_hash = None + + # Store only a capped excerpt of extracted text to reduce DB bloat. + # If encryption isn't configured, skip encryption but do not break scanning. + try: + if extracted_text: + extracted_text_encrypted = encrypt(extracted_text[:20000]) + except Exception: + extracted_text_encrypted = None + + # delegate to existing implementation + # NOTE: evidence_ui.scan is the "real" scanner implementation. + # We keep this router thin and focused on integration concerns. + scan_result = await evidence_ui.scan( + evidence=evidence, + strategy_name=strategy_name, + user_id=str(current_user.id), + ) + + # --- Append validator to response (without changing existing keys) --- + ok_value: bool | None = None + response_payload: dict | None = None + + if isinstance(scan_result, dict): + response_payload = scan_result + ok_value = bool(response_payload.get("ok")) if "ok" in response_payload else None + if ok_value is True and validator_payload is not None: + response_payload["validator"] = validator_payload + elif isinstance(scan_result, JSONResponse): + try: + payload = json.loads((scan_result.body or b"{}").decode("utf-8")) + except Exception: + payload = None + if isinstance(payload, dict): + response_payload = payload + ok_value = bool(payload.get("ok")) if "ok" in payload else None + if ok_value is True and validator_payload is not None: + payload["validator"] = validator_payload + # Return a new JSONResponse to include validator payload. + scan_result = JSONResponse(payload, status_code=scan_result.status_code) + + # --- Persist validator output (best-effort; never blocks scan) --- + try: + status = "success" if ok_value is True else "error" + if validator_payload is not None: + record = EvidenceValidation( + user_id=current_user.id, + strategy_name=strategy_name, + source_filename=getattr(evidence, "filename", None), + text_hash=text_hash, + extracted_text_encrypted=extracted_text_encrypted, + matches_json=validator_payload, + status=status, + ) + db.add(record) + await db.commit() + except Exception: + try: + await db.rollback() + except Exception: + pass + + return scan_result + + +@router.get("/reports/{filename}") +async def download_report(filename: str): + """ + Backend API: download a generated report file. + + The frontend links to this URL using: + - frontend/src/api/client.js -> getEvidenceReportUrl() + - GET /v1/evidence/reports/{filename} + """ + # Reuse existing download handler in security/evidence_ui/app.py + return evidence_ui.download_report(filename) diff --git a/backend-api/app/api/v1/exports.py b/backend-api/app/api/v1/exports.py deleted file mode 100644 index 782c3d58..00000000 --- a/backend-api/app/api/v1/exports.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import APIRouter -from app.schemas.exports import ExportRequest, ExportStatusResponse, ExportResponse -from app.services.job_store import create_export_job, get_job_status -from app.core.errors import NotFound - -router = APIRouter(prefix="/exports", tags=["Exports"]) - -@router.post("/report", response_model=ExportResponse) -def create_report_export(request: ExportRequest): - job_id = create_export_job(scan_id=request.scan_id, fmt=request.format) - return {"job_id": job_id, "status": "pending"} - -@router.get("/{job_id}", response_model=ExportStatusResponse) -def get_export_status(job_id: str): - job = get_job_status(job_id) - if not job: - raise NotFound(f"Export job {job_id}") - return job diff --git a/backend-api/app/api/v1/m365_connections.py b/backend-api/app/api/v1/m365_connections.py new file mode 100644 index 00000000..c5b0a1e0 --- /dev/null +++ b/backend-api/app/api/v1/m365_connections.py @@ -0,0 +1,233 @@ +"""M365 Connection API endpoints.""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.auth import get_current_user +from app.db.session import get_async_session +from app.models.user import User +from app.models.m365_connection import M365Connection +from app.schemas.m365_connection import ( + M365ConnectionCreate, + M365ConnectionRead, + M365ConnectionUpdate, + M365ConnectionTestResult, +) +from app.services.encryption import encrypt, decrypt +from app.services.m365_graph import M365ConnectionError, validate_m365_connection + +router = APIRouter(prefix="/m365-connections", tags=["M365 Connections"]) + + +@router.post("/", response_model=M365ConnectionRead, status_code=status.HTTP_201_CREATED) +async def create_connection( + connection_data: M365ConnectionCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> M365Connection: + """Create a new M365 connection for the current user.""" + # Validate credentials before saving + try: + await validate_m365_connection( + tenant_id=connection_data.tenant_id, + client_id=connection_data.client_id, + client_secret=connection_data.client_secret, + ) + except M365ConnectionError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + connection = M365Connection( + user_id=current_user.id, + name=connection_data.name, + tenant_id=connection_data.tenant_id, + client_id=connection_data.client_id, + encrypted_client_secret=encrypt(connection_data.client_secret), + ) + db.add(connection) + await db.commit() + await db.refresh(connection) + return connection + + +@router.get("/", response_model=list[M365ConnectionRead]) +async def list_connections( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> list[M365Connection]: + """List all M365 connections for the current user.""" + result = await db.execute( + select(M365Connection) + .where(M365Connection.user_id == current_user.id) + .order_by(M365Connection.created_at.desc()) + ) + return list(result.scalars().all()) + + +@router.get("/{connection_id}", response_model=M365ConnectionRead) +async def get_connection( + connection_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> M365Connection: + """Get a specific M365 connection by ID.""" + result = await db.execute( + select(M365Connection).where( + M365Connection.id == connection_id, + M365Connection.user_id == current_user.id, + ) + ) + connection = result.scalar_one_or_none() + if not connection: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Connection {connection_id} not found", + ) + return connection + + +@router.put("/{connection_id}", response_model=M365ConnectionRead) +async def update_connection( + connection_id: int, + update_data: M365ConnectionUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> M365Connection: + """Update an M365 connection.""" + result = await db.execute( + select(M365Connection).where( + M365Connection.id == connection_id, + M365Connection.user_id == current_user.id, + ) + ) + connection = result.scalar_one_or_none() + if not connection: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Connection {connection_id} not found", + ) + + # If tenant_id or client_id changes, require a new secret (can't validate new app without it) + tenant_changed = update_data.tenant_id is not None and update_data.tenant_id != connection.tenant_id + client_changed = update_data.client_id is not None and update_data.client_id != connection.client_id + secret_provided = update_data.client_secret is not None + + if (tenant_changed or client_changed) and not secret_provided: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="client_secret is required when changing tenant_id or client_id", + ) + + # Validate effective credentials if anything credential-related changed + should_validate = tenant_changed or client_changed or secret_provided + if should_validate: + effective_tenant_id = update_data.tenant_id or connection.tenant_id + effective_client_id = update_data.client_id or connection.client_id + effective_secret = ( + update_data.client_secret + if update_data.client_secret is not None + else decrypt(connection.encrypted_client_secret) + ) + try: + await validate_m365_connection( + tenant_id=effective_tenant_id, + client_id=effective_client_id, + client_secret=effective_secret, + ) + except M365ConnectionError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + # Update only provided fields (after validation) + if update_data.name is not None: + connection.name = update_data.name + if update_data.tenant_id is not None: + connection.tenant_id = update_data.tenant_id + if update_data.client_id is not None: + connection.client_id = update_data.client_id + if update_data.client_secret is not None: + connection.encrypted_client_secret = encrypt(update_data.client_secret) + if update_data.is_active is not None: + connection.is_active = update_data.is_active + + await db.commit() + await db.refresh(connection) + return connection + + +@router.delete("/{connection_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_connection( + connection_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> None: + """Delete an M365 connection (hard delete).""" + result = await db.execute( + select(M365Connection).where( + M365Connection.id == connection_id, + M365Connection.user_id == current_user.id, + ) + ) + connection = result.scalar_one_or_none() + if not connection: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Connection {connection_id} not found", + ) + + await db.delete(connection) + await db.commit() + + +@router.post("/{connection_id}/test", response_model=M365ConnectionTestResult) +async def test_connection( + connection_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> M365ConnectionTestResult: + """Test an M365 connection by attempting to authenticate.""" + result = await db.execute( + select(M365Connection).where( + M365Connection.id == connection_id, + M365Connection.user_id == current_user.id, + ) + ) + connection = result.scalar_one_or_none() + if not connection: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Connection {connection_id} not found", + ) + + # Decrypt credentials + client_secret = decrypt(connection.encrypted_client_secret) + + try: + details = await validate_m365_connection( + tenant_id=connection.tenant_id, + client_id=connection.client_id, + client_secret=client_secret, + ) + #update m365 connection modal with validation attributes (what exact error happened when we tried the api call) + return M365ConnectionTestResult( + success=True, + message="Connection successful", + tenant_name=details.tenant_display_name, + tenant_display_name=details.tenant_display_name, + default_domain=details.default_domain, + verified_domains=details.verified_domains, + ) + except M365ConnectionError as e: + return M365ConnectionTestResult( + success=False, + message=str(e), + tenant_name=None, + tenant_display_name=None, + default_domain=None, + verified_domains=[], + ) diff --git a/backend-api/app/api/v1/platforms.py b/backend-api/app/api/v1/platforms.py new file mode 100644 index 00000000..15018666 --- /dev/null +++ b/backend-api/app/api/v1/platforms.py @@ -0,0 +1,41 @@ +"""Platform API endpoints for listing available platform types.""" + +from fastapi import APIRouter, Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.auth import get_current_user +from app.db.session import get_async_session +from app.models.platform import Platform +from app.models.user import User +from app.schemas.platform import PlatformRead + +router = APIRouter(prefix="/platforms", tags=["Platforms"]) + + +@router.get("", response_model=list[PlatformRead]) +async def list_platforms( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> list[PlatformRead]: + """List active platforms. + + Returns only platforms where is_active=True (currently only M365). + """ + result = await db.execute( + select(Platform).where(Platform.is_active == True).order_by(Platform.name) + ) + return list(result.scalars().all()) + + +@router.get("/all", response_model=list[PlatformRead]) +async def list_all_platforms( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> list[PlatformRead]: + """List all platforms including inactive ones. + + Useful for showing which platforms will be supported in the future. + """ + result = await db.execute(select(Platform).order_by(Platform.name)) + return list(result.scalars().all()) diff --git a/backend-api/app/api/v1/router.py b/backend-api/app/api/v1/router.py index 611e079d..c59a8758 100644 --- a/backend-api/app/api/v1/router.py +++ b/backend-api/app/api/v1/router.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from app.api.v1 import exports, audit, auth, test +from app.api.v1 import auth, test, evidence, m365_connections, scans, benchmarks, platforms api_router = APIRouter() @@ -9,6 +9,17 @@ # Test routes api_router.include_router(test.router) -# Existing routes -api_router.include_router(exports.router) -api_router.include_router(audit.router) +# Platform routes (list available platforms) +api_router.include_router(platforms.router) + +# Cloud connection routes +api_router.include_router(m365_connections.router) + +# Scan routes +api_router.include_router(scans.router) + +# Benchmark discovery routes +api_router.include_router(benchmarks.router) + +# Evidence routes +api_router.include_router(evidence.router) diff --git a/backend-api/app/api/v1/scans.py b/backend-api/app/api/v1/scans.py new file mode 100644 index 00000000..5282d064 --- /dev/null +++ b/backend-api/app/api/v1/scans.py @@ -0,0 +1,186 @@ +"""Scan API endpoints.""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from app.core.auth import get_current_user +from app.db.session import get_async_session +from app.models.compliance import Scan +from app.models.m365_connection import M365Connection +from app.models.scan_result import ScanResult +from app.models.user import User +from app.schemas.scan import ( + ScanCreate, + ScanCreatedResponse, + ScanListItem, + ScanRead, + ScanResultRead, +) +from app.services.benchmark_reader import get_file_reader +from app.services.celery_client import queue_scan + +router = APIRouter(prefix="/scans", tags=["Scans"]) + + +@router.post("/", response_model=ScanCreatedResponse, status_code=status.HTTP_201_CREATED) +async def create_scan( + scan_data: ScanCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> ScanCreatedResponse: + """Create a new compliance scan. + + Creates a scan record with all ScanResult records and queues a Celery task. + The scan runs asynchronously - poll GET /scans/{id} for status. + """ + # Verify the connection exists and belongs to the user + result = await db.execute( + select(M365Connection).where( + M365Connection.id == scan_data.m365_connection_id, + M365Connection.user_id == current_user.id, + M365Connection.is_active == True, + ) + ) + connection = result.scalar_one_or_none() + if not connection: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"M365 connection {scan_data.m365_connection_id} not found or inactive", + ) + + # Load benchmark metadata to get all controls + file_reader = get_file_reader() + try: + all_controls = file_reader.list_controls( + scan_data.framework, scan_data.benchmark, scan_data.version + ) + except FileNotFoundError: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Benchmark {scan_data.framework}/{scan_data.benchmark}/{scan_data.version} not found", + ) + + # Validate platform matches (benchmark must be for m365) + metadata = file_reader.get_benchmark_metadata( + scan_data.framework, scan_data.benchmark, scan_data.version + ) + if metadata.get("platform", "").lower() != "m365": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Benchmark platform '{metadata.get('platform')}' does not match M365 connection", + ) + + # Create scan record + scan = Scan( + user_id=current_user.id, + m365_connection_id=scan_data.m365_connection_id, + framework=scan_data.framework, + benchmark=scan_data.benchmark, + version=scan_data.version, + status="pending", + total_controls=len(all_controls), + ) + db.add(scan) + await db.flush() # Get scan.id + + # Create ScanResult records for ALL controls + selected_ids = set(scan_data.control_ids) if scan_data.control_ids else None + skipped = 0 + for control in all_controls: + is_selected = selected_ids is None or control["control_id"] in selected_ids + result_status = "pending" if is_selected else "skipped" + if result_status == "skipped": + skipped += 1 + scan_result = ScanResult( + scan_id=scan.id, + control_id=control["control_id"], + status=result_status, + ) + db.add(scan_result) + + scan.skipped_count = skipped + await db.commit() + await db.refresh(scan) + + # Queue Celery task + task = queue_scan(scan.id) + + return ScanCreatedResponse( + id=scan.id, + status="pending", + message=f"Scan queued successfully. Task ID: {task.id}", + ) + + +@router.get("/", response_model=list[ScanListItem]) +async def list_scans( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), + limit: int = 50, + offset: int = 0, +) -> list[Scan]: + """List scans for the current user.""" + result = await db.execute( + select(Scan) + .options(selectinload(Scan.m365_connection)) + .where(Scan.user_id == current_user.id) + .order_by(Scan.started_at.desc()) + .limit(limit) + .offset(offset) + ) + return list(result.scalars().all()) + + +@router.get("/{scan_id}", response_model=ScanRead) +async def get_scan( + scan_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +) -> Scan: + """Get scan details by ID including results.""" + result = await db.execute( + select(Scan) + .options(selectinload(Scan.results), selectinload(Scan.m365_connection)) + .where(Scan.id == scan_id, Scan.user_id == current_user.id) + ) + scan = result.scalar_one_or_none() + if not scan: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Scan {scan_id} not found", + ) + return scan + + +@router.get("/{scan_id}/results", response_model=list[ScanResultRead]) +async def get_scan_results( + scan_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), + status_filter: str | None = None, +) -> list[ScanResult]: + """Get results for a scan. + + Optionally filter by status (pending, passed, failed, error, skipped). + """ + # Verify scan exists and user has access + scan_result = await db.execute( + select(Scan).where(Scan.id == scan_id, Scan.user_id == current_user.id) + ) + scan = scan_result.scalar_one_or_none() + if not scan: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Scan {scan_id} not found", + ) + + # Build query + query = select(ScanResult).where(ScanResult.scan_id == scan_id) + if status_filter: + query = query.where(ScanResult.status == status_filter) + query = query.order_by(ScanResult.control_id) + + results = await db.execute(query) + return list(results.scalars().all()) diff --git a/backend-api/app/core/config.py b/backend-api/app/core/config.py index b378d686..8a87af82 100644 --- a/backend-api/app/core/config.py +++ b/backend-api/app/core/config.py @@ -15,6 +15,28 @@ class Settings(BaseSettings): ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + # Public URLs (used for OAuth redirects) + # These must be the externally reachable URLs (e.g. localhost from the browser). + BACKEND_PUBLIC_URL: str = "http://localhost:8000" + FRONTEND_URL: str = "http://localhost:3000" + + # Google OAuth (SSO) + GOOGLE_OAUTH_CLIENT_ID: str = "" + GOOGLE_OAUTH_CLIENT_SECRET: str = "" + + # Redis (for Celery broker) + REDIS_URL: str = "redis://localhost:6379" + + # OPA (Open Policy Agent) + OPA_URL: str = "http://localhost:8181" + + # Encryption (for securing credentials at rest) + # Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" + ENCRYPTION_KEY: str = "" + + # Policies directory (for benchmark/control metadata) + POLICIES_DIR: str = "/app/policies" + class Config: env_file = ".env" diff --git a/backend-api/app/core/users.py b/backend-api/app/core/users.py index 3e3e1418..d8440471 100644 --- a/backend-api/app/core/users.py +++ b/backend-api/app/core/users.py @@ -24,6 +24,7 @@ from app.core.config import get_settings from app.db.session import get_async_session from app.models.user import User +from app.models.oauth_account import OAuthAccount settings = get_settings() @@ -53,7 +54,7 @@ async def on_after_request_verify( async def get_user_db(session: AsyncSession = Depends(get_async_session)): """Dependency for getting the user database.""" - yield SQLAlchemyUserDatabase(session, User) + yield SQLAlchemyUserDatabase(session, User, OAuthAccount) async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): diff --git a/backend-api/app/db/init_db.py b/backend-api/app/db/init_db.py index a10db965..8c21bbcd 100644 --- a/backend-api/app/db/init_db.py +++ b/backend-api/app/db/init_db.py @@ -5,45 +5,61 @@ python -m app.db.init_db """ import asyncio + from sqlalchemy import select + from app.db.session import async_session_maker from app.models.user import User, Role from fastapi_users.password import PasswordHelper async def init_db(): - """Initialize database with default admin user.""" + """ + Database seeding for local/dev environments. + + IMPORTANT: + - Passwords are stored hashed in the DB (see User.hashed_password). + - This script will create OR update a default admin user for local development. + """ + admin_email = "admin@example.com" + admin_password = "admin" + password_helper = PasswordHelper() async with async_session_maker() as session: - # Check if admin user already exists + # Look up the canonical seed user. result = await session.execute( - select(User).where(User.email == "admin@example.com") + select(User).where(User.email == admin_email) ) - existing_user = result.scalar_one_or_none() + # Be resilient to relationship eager-loads that can duplicate rows. + existing_user = result.unique().scalar_one_or_none() + created = False if existing_user: - print("Admin user already exists. Skipping seed.") - return - - # Create default admin user - admin_user = User( - email="admin@example.com", - hashed_password=password_helper.hash("admin"), - role=Role.ADMIN.value, - is_active=True, - is_superuser=True, - is_verified=True, - ) + admin_user = existing_user + else: + created = True + admin_user = User(email=admin_email) + session.add(admin_user) + + # Ensure the account is a usable admin for local development. + admin_user.hashed_password = password_helper.hash(admin_password) + admin_user.role = Role.ADMIN.value + admin_user.is_active = True + admin_user.is_superuser = True + admin_user.is_verified = True - session.add(admin_user) await session.commit() - print("[SUCCESS] Created default admin user with the following details.") - print(f" Email: admin@example.com") - print(f" Password: admin") + print( + "[SUCCESS] Created default admin user with the following details." + if created + else "[SUCCESS] Updated default admin user with the following details." + ) + print(f" Email: {admin_email}") + print(f" Password: {admin_password}") print(f" Role: {Role.ADMIN.value}") - print("\nIMPORTANT: We need to make sure the default password is reset at first login.") + print("\nIMPORTANT: Change this password after first login.") if __name__ == "__main__": diff --git a/backend-api/app/main.py b/backend-api/app/main.py index 94d70014..1c8710a9 100644 --- a/backend-api/app/main.py +++ b/backend-api/app/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from app.core.logging import setup_logging from app.api.v1.router import api_router from app.core.config import get_settings @@ -12,7 +13,19 @@ def create_app() -> FastAPI: setup_logging() app = FastAPI(title="AutoAudit API", version="0.1.0") + # RequestLoggingMiddleware must be added before CORSMiddleware + # (middleware executes in reverse order - last added runs first) app.add_middleware(RequestLoggingMiddleware) + + # Allow frontend (localhost:3000 and others) to call the API during development. + # CORS must be added last so it runs first and wraps all responses including errors. + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # permissive for dev; adjust in prod + allow_credentials=False, # must be False when using wildcard origins + allow_methods=["*"], + allow_headers=["*"], + ) app.include_router(api_router, prefix=settings.API_PREFIX) # error handler diff --git a/backend-api/app/models/__init__.py b/backend-api/app/models/__init__.py index ef7db987..9e6e8e80 100644 --- a/backend-api/app/models/__init__.py +++ b/backend-api/app/models/__init__.py @@ -1,12 +1,26 @@ """Database models for the AutoAudit backend API.""" + from app.models.user import User, Role -from app.models.compliance import Tenant, Rule, Scan, Issue +from app.models.oauth_account import OAuthAccount +from app.models.m365_connection import M365Connection +from app.models.azure_connection import AzureConnection +from app.models.gcp_connection import GCPConnection +from app.models.aws_connection import AWSConnection +from app.models.platform import Platform +from app.models.scan_result import ScanResult +from app.models.compliance import Scan +from app.models.evidence_validation import EvidenceValidation __all__ = [ "User", "Role", - "Tenant", - "Rule", + "OAuthAccount", + "M365Connection", + "AzureConnection", + "GCPConnection", + "AWSConnection", + "Platform", + "ScanResult", "Scan", - "Issue", + "EvidenceValidation", ] diff --git a/backend-api/app/models/aws_connection.py b/backend-api/app/models/aws_connection.py new file mode 100644 index 00000000..08284b8c --- /dev/null +++ b/backend-api/app/models/aws_connection.py @@ -0,0 +1,13 @@ +"""AWS connection model stub.""" + +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base import Base + + +class AWSConnection(Base): + """AWS connection stub (columns added when implemented).""" + + __tablename__ = "aws_connection" + + id: Mapped[int] = mapped_column(primary_key=True) diff --git a/backend-api/app/models/azure_connection.py b/backend-api/app/models/azure_connection.py new file mode 100644 index 00000000..5f6f0651 --- /dev/null +++ b/backend-api/app/models/azure_connection.py @@ -0,0 +1,13 @@ +"""Azure connection model stub.""" + +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base import Base + + +class AzureConnection(Base): + """Azure connection stub (columns added when implemented).""" + + __tablename__ = "azure_connection" + + id: Mapped[int] = mapped_column(primary_key=True) diff --git a/backend-api/app/models/compliance.py b/backend-api/app/models/compliance.py index 03fb7e59..178e928f 100644 --- a/backend-api/app/models/compliance.py +++ b/backend-api/app/models/compliance.py @@ -1,79 +1,77 @@ -"""Compliance models for audit scans and findings.""" +"""Compliance models for audit scans.""" + from datetime import datetime from decimal import Decimal -from typing import Optional +from typing import TYPE_CHECKING, Optional -from sqlalchemy import ForeignKey, String, Text, Numeric -from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy import ForeignKey, Numeric, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from app.db.base import Base - -class Tenant(Base): - """Tenant represents a company or organization being audited.""" - __tablename__ = "tenant" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(255), nullable=False) - external_tenant_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) - created_at: Mapped[datetime] = mapped_column(server_default=func.now()) - - # Relationships - scans: Mapped[list["Scan"]] = relationship(back_populates="tenant") - - -class Rule(Base): - """Rule represents a compliance control from a framework (e.g., CIS, Essential 8).""" - __tablename__ = "rule" - - id: Mapped[int] = mapped_column(primary_key=True) - framework: Mapped[str] = mapped_column(String(100), nullable=False) - control_key: Mapped[str] = mapped_column(String(100), nullable=False) - title: Mapped[str] = mapped_column(Text, nullable=False) - severity: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) - description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) - - # Relationships - issues: Mapped[list["Issue"]] = relationship(back_populates="rule") +if TYPE_CHECKING: + from app.models.m365_connection import M365Connection + from app.models.scan_result import ScanResult + from app.models.user import User class Scan(Base): - """Scan represents an individual compliance scan run against a tenant.""" + """Scan represents an individual compliance scan run against a cloud connection.""" + __tablename__ = "scan" id: Mapped[int] = mapped_column(primary_key=True) - tenant_id: Mapped[int] = mapped_column(ForeignKey("tenant.id"), nullable=False) + + # Direct user ownership + user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), nullable=False) + + # Cloud connection FKs (only one should be non-null per scan) + m365_connection_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("m365_connection.id"), nullable=True + ) + azure_connection_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("azure_connection.id"), nullable=True + ) + gcp_connection_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("gcp_connection.id"), nullable=True + ) + aws_connection_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("aws_connection.id"), nullable=True + ) + + # Framework/benchmark targeting + framework: Mapped[str] = mapped_column(String(50), nullable=False) + benchmark: Mapped[str] = mapped_column(String(100), nullable=False) + version: Mapped[str] = mapped_column(String(20), nullable=False) + # Note: control_ids removed - ScanResult records track which controls were selected + + # Scan timing and status started_at: Mapped[datetime] = mapped_column(server_default=func.now()) finished_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) - status: Mapped[str] = mapped_column(String(30), default="running") - compliance_score: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2), nullable=True) + status: Mapped[str] = mapped_column(String(30), default="pending") + + # Results summary + compliance_score: Mapped[Optional[Decimal]] = mapped_column( + Numeric(5, 2), nullable=True + ) total_controls: Mapped[int] = mapped_column(default=0) passed_count: Mapped[int] = mapped_column(default=0) failed_count: Mapped[int] = mapped_column(default=0) - not_tested_count: Mapped[int] = mapped_column(default=0) + skipped_count: Mapped[int] = mapped_column(default=0) + error_count: Mapped[int] = mapped_column(default=0) notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # Relationships - tenant: Mapped["Tenant"] = relationship(back_populates="scans") - issues: Mapped[list["Issue"]] = relationship(back_populates="scan") - - -class Issue(Base): - """Issue represents a finding from a compliance scan.""" - __tablename__ = "issue" - - id: Mapped[int] = mapped_column(primary_key=True) - scan_id: Mapped[int] = mapped_column(ForeignKey("scan.id"), nullable=False) - rule_id: Mapped[Optional[int]] = mapped_column(ForeignKey("rule.id"), nullable=True) - priority: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) - title: Mapped[Optional[str]] = mapped_column(Text, nullable=True) - description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) - result: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) - evidence: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) - created_at: Mapped[datetime] = mapped_column(server_default=func.now()) - - # Relationships - scan: Mapped["Scan"] = relationship(back_populates="issues") - rule: Mapped[Optional["Rule"]] = relationship(back_populates="issues") + user: Mapped["User"] = relationship(back_populates="scans") + m365_connection: Mapped[Optional["M365Connection"]] = relationship( + back_populates="scans" + ) + results: Mapped[list["ScanResult"]] = relationship(back_populates="scan") + + @property + def connection_name(self) -> str | None: + """Convenience field for API/UI display.""" + if self.m365_connection is not None: + return self.m365_connection.name + return None diff --git a/backend-api/app/models/evidence_validation.py b/backend-api/app/models/evidence_validation.py new file mode 100644 index 00000000..697c7725 --- /dev/null +++ b/backend-api/app/models/evidence_validation.py @@ -0,0 +1,45 @@ +"""EvidenceValidation model for persisting validator outputs.""" + +from __future__ import annotations + +from datetime import datetime +from typing import TYPE_CHECKING, Optional + +from sqlalchemy import ForeignKey, String, Text +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.sql import func + +from app.db.base import Base + +if TYPE_CHECKING: + from app.models.user import User + + +class EvidenceValidation(Base): + """Persisted validator output for an evidence scan.""" + + __tablename__ = "evidence_validation" + + id: Mapped[int] = mapped_column(primary_key=True) + + created_at: Mapped[datetime] = mapped_column(server_default=func.now()) + + user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), nullable=False) + strategy_name: Mapped[str] = mapped_column(String(255), nullable=False) + source_filename: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) + + # sha256 of extracted text (hex) + text_hash: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) + + # Optional encrypted extracted text (or excerpt) for secure traceability + extracted_text_encrypted: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + # Validator output JSON (matched/missing/summary) + matches_json: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) + + # success | error + status: Mapped[str] = mapped_column(String(25), nullable=False, default="success") + + # Relationships + user: Mapped["User"] = relationship(back_populates="evidence_validations") diff --git a/backend-api/app/models/gcp_connection.py b/backend-api/app/models/gcp_connection.py new file mode 100644 index 00000000..508cc378 --- /dev/null +++ b/backend-api/app/models/gcp_connection.py @@ -0,0 +1,13 @@ +"""GCP connection model stub.""" + +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base import Base + + +class GCPConnection(Base): + """GCP connection stub (columns added when implemented).""" + + __tablename__ = "gcp_connection" + + id: Mapped[int] = mapped_column(primary_key=True) diff --git a/backend-api/app/models/m365_connection.py b/backend-api/app/models/m365_connection.py new file mode 100644 index 00000000..aa05afd6 --- /dev/null +++ b/backend-api/app/models/m365_connection.py @@ -0,0 +1,51 @@ +"""M365 Connection model for storing Microsoft 365 tenant credentials.""" + +from datetime import datetime +from typing import TYPE_CHECKING + +from sqlalchemy import ForeignKey, String, Text, Boolean +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.sql import func + +from app.db.base import Base + +if TYPE_CHECKING: + from app.models.user import User + from app.models.compliance import Scan + + +class M365Connection(Base): + """M365Connection stores credentials for connecting to a Microsoft 365 tenant. + + The client_secret is encrypted at rest using Fernet symmetric encryption. + Users can have multiple M365 connections (e.g., for different clients/tenants). + """ + + __tablename__ = "m365_connection" + + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), nullable=False) + + # Human-readable name for this connection (e.g., "Contoso Production") + name: Mapped[str] = mapped_column(String(255), nullable=False) + + # Azure AD / Entra ID tenant GUID + tenant_id: Mapped[str] = mapped_column(String(255), nullable=False) + + # Azure AD App Registration client ID + client_id: Mapped[str] = mapped_column(String(255), nullable=False) + + # Encrypted client secret (Fernet encryption) + encrypted_client_secret: Mapped[str] = mapped_column(Text, nullable=False) + + # Soft active flag - allows deactivating without deleting + is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) + + created_at: Mapped[datetime] = mapped_column(server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + server_default=func.now(), onupdate=func.now() + ) + + # Relationships + user: Mapped["User"] = relationship(back_populates="m365_connections") + scans: Mapped[list["Scan"]] = relationship(back_populates="m365_connection") diff --git a/backend-api/app/models/oauth_account.py b/backend-api/app/models/oauth_account.py new file mode 100644 index 00000000..c700e6b5 --- /dev/null +++ b/backend-api/app/models/oauth_account.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING + +from fastapi_users.db import SQLAlchemyBaseOAuthAccountTable +from sqlalchemy import ForeignKey, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.db.base import Base + +if TYPE_CHECKING: + from app.models.user import User + + +class OAuthAccount(SQLAlchemyBaseOAuthAccountTable[int], Base): + """OAuth account linked to a User (e.g., Google identity).""" + + __tablename__ = "oauth_account" + __table_args__ = ( + UniqueConstraint( + "oauth_name", + "account_id", + name="uq_oauth_account_oauth_name_account_id", + ), + ) + + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("user.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + + user: Mapped["User"] = relationship(back_populates="oauth_accounts") + + + diff --git a/backend-api/app/models/platform.py b/backend-api/app/models/platform.py new file mode 100644 index 00000000..97cfcbec --- /dev/null +++ b/backend-api/app/models/platform.py @@ -0,0 +1,17 @@ +"""Platform model for supported platform types.""" + +from sqlalchemy import Boolean, String +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base import Base + + +class Platform(Base): + """Lookup table for supported platform types (M365, Azure, GCP, AWS).""" + + __tablename__ = "platform" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) + display_name: Mapped[str] = mapped_column(String(100), nullable=False) + is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) diff --git a/backend-api/app/models/scan_result.py b/backend-api/app/models/scan_result.py new file mode 100644 index 00000000..23be59f1 --- /dev/null +++ b/backend-api/app/models/scan_result.py @@ -0,0 +1,40 @@ +"""ScanResult model for individual control results within a scan.""" + +from datetime import datetime +from typing import TYPE_CHECKING, Optional + +from sqlalchemy import ForeignKey, String, Text, UniqueConstraint +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.sql import func + +from app.db.base import Base + +if TYPE_CHECKING: + from app.models.compliance import Scan + + +class ScanResult(Base): + """Individual control result within a scan.""" + + __tablename__ = "scan_result" + __table_args__ = ( + UniqueConstraint("scan_id", "control_id", name="uq_scan_result_scan_control"), + ) + + id: Mapped[int] = mapped_column(primary_key=True) + scan_id: Mapped[int] = mapped_column(ForeignKey("scan.id"), nullable=False) + control_id: Mapped[str] = mapped_column(String(50), nullable=False) + + # Status: pending, passed, failed, error, skipped + status: Mapped[str] = mapped_column(String(20), nullable=False) + message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + evidence: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) + + created_at: Mapped[datetime] = mapped_column(server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + server_default=func.now(), onupdate=func.now() + ) + + # Relationships + scan: Mapped["Scan"] = relationship(back_populates="results") diff --git a/backend-api/app/models/user.py b/backend-api/app/models/user.py index 23944571..d409ba64 100644 --- a/backend-api/app/models/user.py +++ b/backend-api/app/models/user.py @@ -1,9 +1,18 @@ from enum import Enum +from typing import TYPE_CHECKING + from fastapi_users.db import SQLAlchemyBaseUserTable from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship + from app.db.base import Base +if TYPE_CHECKING: + from app.models.compliance import Scan + from app.models.evidence_validation import EvidenceValidation + from app.models.m365_connection import M365Connection + from app.models.oauth_account import OAuthAccount + class Role(str, Enum): """User roles for role-based access control.""" @@ -32,3 +41,17 @@ class User(SQLAlchemyBaseUserTable[int], Base): # - is_active: bool # - is_superuser: bool # - is_verified: bool + + # Relationships + oauth_accounts: Mapped[list["OAuthAccount"]] = relationship( + back_populates="user", + cascade="all, delete-orphan", + lazy="selectin", + ) + m365_connections: Mapped[list["M365Connection"]] = relationship( + back_populates="user" + ) + scans: Mapped[list["Scan"]] = relationship(back_populates="user") + evidence_validations: Mapped[list["EvidenceValidation"]] = relationship( + back_populates="user" + ) diff --git a/backend-api/app/schemas/__init__.py b/backend-api/app/schemas/__init__.py index fe02af45..e6b5cead 100644 --- a/backend-api/app/schemas/__init__.py +++ b/backend-api/app/schemas/__init__.py @@ -1,14 +1,16 @@ """Pydantic schemas for request/response validation.""" from app.schemas.user import UserRead, UserCreate, UserUpdate -from app.schemas.audit import AuditLog -from app.schemas.exports import ExportRequest, ExportResponse, ExportStatusResponse +from app.schemas.evidence_validation import EvidenceValidationRead +from app.schemas.validator_match import ValidatorMatch +from app.schemas.validator_result import ValidatorResult +from app.schemas.validator_summary import ValidatorSummary __all__ = [ "UserRead", "UserCreate", "UserUpdate", - "AuditLog", - "ExportRequest", - "ExportResponse", - "ExportStatusResponse", + "EvidenceValidationRead", + "ValidatorMatch", + "ValidatorResult", + "ValidatorSummary", ] diff --git a/backend-api/app/schemas/audit.py b/backend-api/app/schemas/audit.py deleted file mode 100644 index d975e72b..00000000 --- a/backend-api/app/schemas/audit.py +++ /dev/null @@ -1,7 +0,0 @@ -from pydantic import BaseModel - -class AuditLog(BaseModel): - timestamp: str - action: str - resource: str - id: str diff --git a/backend-api/app/schemas/benchmark.py b/backend-api/app/schemas/benchmark.py new file mode 100644 index 00000000..996651e2 --- /dev/null +++ b/backend-api/app/schemas/benchmark.py @@ -0,0 +1,34 @@ +"""Benchmark and control schemas for API responses.""" + +from pydantic import BaseModel + + +class BenchmarkRead(BaseModel): + """Schema for benchmark data read from metadata.json files.""" + + framework: str + slug: str + version: str + name: str + platform: str + release_date: str | None = None + source_url: str | None = None + control_count: int + + +class ControlRead(BaseModel): + """Schema for control data read from metadata.json files.""" + + control_id: str + title: str + description: str | None = None + severity: str | None = None + service: str | None = None + level: str # Benchmark level (e.g., "L1", "L2" for CIS; varies by framework) + is_manual: bool # True if control has no API, always manual + benchmark_audit_type: str # What the benchmark says: "Automated" or "Manual" + automation_status: str # ready, deferred, blocked, manual, not_started + data_collector_id: str | None = None # Null for manual controls + policy_file: str | None = None # Null for manual controls + requires_permissions: list[str] | None = None + notes: str | None = None # Blockers, special considerations diff --git a/backend-api/app/schemas/evidence_validation.py b/backend-api/app/schemas/evidence_validation.py new file mode 100644 index 00000000..63e6cffe --- /dev/null +++ b/backend-api/app/schemas/evidence_validation.py @@ -0,0 +1,25 @@ +"""Pydantic schema for evidence validation.""" + +from datetime import datetime + +from pydantic import BaseModel, ConfigDict + +from app.schemas.validator_result import ValidatorResult + + +class EvidenceValidationRead(BaseModel): + """Schema for reading evidence validation records.""" + + id: int + user_id: int + strategy_name: str + source_filename: str | None + text_hash: str | None + # Note: extracted_text_encrypted intentionally omitted - sensitive data shouldn't be in API responses + matches_json: ValidatorResult | None + status: str + created_at: datetime + + model_config = ConfigDict(from_attributes=True) + + diff --git a/backend-api/app/schemas/exports.py b/backend-api/app/schemas/exports.py deleted file mode 100644 index 1ff91364..00000000 --- a/backend-api/app/schemas/exports.py +++ /dev/null @@ -1,17 +0,0 @@ -from pydantic import BaseModel, Field - -class ExportRequest(BaseModel): - scan_id: str = Field(..., example="s-001") - format: str = Field(..., pattern="^(csv|pdf)$", example="csv") - -class ExportResponse(BaseModel): - job_id: str - status: str - -class ExportStatusResponse(BaseModel): - job_id: str - scan_id: str - format: str - status: str - created_at: str - url: str | None = None diff --git a/backend-api/app/schemas/m365_connection.py b/backend-api/app/schemas/m365_connection.py new file mode 100644 index 00000000..f32a3945 --- /dev/null +++ b/backend-api/app/schemas/m365_connection.py @@ -0,0 +1,56 @@ +"""Pydantic schemas for M365 connections.""" + +from datetime import datetime + +from pydantic import BaseModel, Field + + +class M365ConnectionBase(BaseModel): + """Base schema for M365 connection.""" + + name: str = Field(..., min_length=1, max_length=255, description="Friendly name for this connection") + tenant_id: str = Field(..., min_length=1, max_length=255, description="Azure AD tenant GUID") + client_id: str = Field(..., min_length=1, max_length=255, description="App registration client ID") + + +class M365ConnectionCreate(M365ConnectionBase): + """Schema for creating an M365 connection.""" + + client_secret: str = Field(..., min_length=1, description="App registration client secret") + + +class M365ConnectionUpdate(BaseModel): + """Schema for updating an M365 connection.""" + + name: str | None = Field(None, min_length=1, max_length=255) + tenant_id: str | None = Field(None, min_length=1, max_length=255) + client_id: str | None = Field(None, min_length=1, max_length=255) + client_secret: str | None = Field(None, min_length=1, description="New client secret (if changing)") + is_active: bool | None = None + + +class M365ConnectionRead(M365ConnectionBase): + """Schema for reading an M365 connection (without secret).""" + + id: int + user_id: int + is_active: bool + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +class M365ConnectionTestResult(BaseModel): + """Schema for connection test result.""" + + success: bool + message: str + # Backwards-compatible display field (legacy) + tenant_name: str | None = None + + # Preferred structured tenant details + tenant_display_name: str | None = None + default_domain: str | None = None + verified_domains: list[str] | None = None diff --git a/backend-api/app/schemas/platform.py b/backend-api/app/schemas/platform.py new file mode 100644 index 00000000..45579819 --- /dev/null +++ b/backend-api/app/schemas/platform.py @@ -0,0 +1,14 @@ +"""Platform schemas for API responses.""" + +from pydantic import BaseModel, ConfigDict + + +class PlatformRead(BaseModel): + """Schema for platform data.""" + + id: int + name: str + display_name: str + is_active: bool + + model_config = ConfigDict(from_attributes=True) diff --git a/backend-api/app/schemas/scan.py b/backend-api/app/schemas/scan.py new file mode 100644 index 00000000..a24fd57f --- /dev/null +++ b/backend-api/app/schemas/scan.py @@ -0,0 +1,103 @@ +"""Pydantic schemas for compliance scans.""" + +from datetime import datetime +from decimal import Decimal + +from pydantic import BaseModel, ConfigDict, Field + + +class ScanCreate(BaseModel): + """Schema for creating a new scan.""" + + m365_connection_id: int = Field( + ..., description="ID of the M365 connection to scan" + ) + framework: str = Field( + ..., min_length=1, max_length=50, description="Framework (e.g., 'cis')" + ) + benchmark: str = Field( + ..., + min_length=1, + max_length=100, + description="Benchmark slug (e.g., 'microsoft-365-foundations')", + ) + version: str = Field( + ..., min_length=1, max_length=20, description="Version (e.g., 'v3.1.0')" + ) + control_ids: list[str] | None = Field( + None, description="Specific control IDs to scan (null = all)" + ) + + +class ScanResultRead(BaseModel): + """Schema for reading scan result details.""" + + id: int + scan_id: int + control_id: str + status: str # pending, passed, failed, error, skipped + message: str | None + evidence: dict | None + created_at: datetime + updated_at: datetime + + model_config = ConfigDict(from_attributes=True) + + +class ScanRead(BaseModel): + """Schema for reading scan details.""" + + id: int + user_id: int + m365_connection_id: int | None + connection_name: str | None = None + azure_connection_id: int | None + gcp_connection_id: int | None + aws_connection_id: int | None + framework: str + benchmark: str + version: str + status: str + started_at: datetime + finished_at: datetime | None + compliance_score: Decimal | None + total_controls: int + passed_count: int + failed_count: int + skipped_count: int + error_count: int + notes: str | None + results: list[ScanResultRead] | None = None + + model_config = ConfigDict(from_attributes=True) + + +class ScanListItem(BaseModel): + """Schema for scan list items (abbreviated).""" + + id: int + user_id: int + m365_connection_id: int | None + connection_name: str | None = None + framework: str + benchmark: str + version: str + status: str + started_at: datetime + finished_at: datetime | None + compliance_score: Decimal | None + total_controls: int + passed_count: int + failed_count: int + skipped_count: int + error_count: int + + model_config = ConfigDict(from_attributes=True) + + +class ScanCreatedResponse(BaseModel): + """Response schema for scan creation.""" + + id: int + status: str + message: str diff --git a/backend-api/app/schemas/user.py b/backend-api/app/schemas/user.py index e3a1530f..46363a2c 100644 --- a/backend-api/app/schemas/user.py +++ b/backend-api/app/schemas/user.py @@ -13,6 +13,15 @@ class UserCreate(schemas.BaseUserCreate): role: Role = Role.VIEWER +class UserRegister(schemas.BaseUserCreate): + """ + Public registration schema. + + Intentionally does NOT expose role/is_superuser/etc so a self-registering + user cannot elevate privileges. + """ + + class UserUpdate(schemas.BaseUserUpdate): """Schema for updating user data.""" role: Role | None = None diff --git a/backend-api/app/schemas/validator_match.py b/backend-api/app/schemas/validator_match.py new file mode 100644 index 00000000..12cc7fae --- /dev/null +++ b/backend-api/app/schemas/validator_match.py @@ -0,0 +1,12 @@ +"""Pydantic schema for a validator match item.""" + +from pydantic import BaseModel + + +class ValidatorMatch(BaseModel): + """A single matched term with occurrence count.""" + + term: str + count: int + + diff --git a/backend-api/app/schemas/validator_result.py b/backend-api/app/schemas/validator_result.py new file mode 100644 index 00000000..d1a05f4e --- /dev/null +++ b/backend-api/app/schemas/validator_result.py @@ -0,0 +1,16 @@ +"""Pydantic schema for validator output.""" + +from pydantic import BaseModel + +from app.schemas.validator_match import ValidatorMatch +from app.schemas.validator_summary import ValidatorSummary + + +class ValidatorResult(BaseModel): + """Validator output structure.""" + + matched: list[ValidatorMatch] + missing: list[str] + summary: ValidatorSummary + + diff --git a/backend-api/app/schemas/validator_summary.py b/backend-api/app/schemas/validator_summary.py new file mode 100644 index 00000000..be9abc3e --- /dev/null +++ b/backend-api/app/schemas/validator_summary.py @@ -0,0 +1,12 @@ +"""Pydantic schema for validator summary counts.""" + +from pydantic import BaseModel, Field + + +class ValidatorSummary(BaseModel): + """Summary counts for validator results.""" + + matchedCount: int = Field(..., description="Number of terms found") + totalTerms: int = Field(..., description="Total terms checked") + + diff --git a/backend-api/app/services/benchmark_reader.py b/backend-api/app/services/benchmark_reader.py new file mode 100644 index 00000000..7d21fc57 --- /dev/null +++ b/backend-api/app/services/benchmark_reader.py @@ -0,0 +1,123 @@ +"""Read benchmark and control metadata from policy files at runtime. + +Policies are organized as: + policies/{framework}/{benchmark-slug}/{version}/metadata.json + policies/{framework}/{benchmark-slug}/{version}/*.rego + +Example: + policies/cis/microsoft-365-foundations/v3.1.0/metadata.json +""" + +from __future__ import annotations + +import json +from functools import lru_cache +from pathlib import Path +from typing import Any + +from app.core.config import get_settings + + +class BenchmarkFileReader: + """Read benchmark and control metadata from files at runtime.""" + + def __init__(self, policies_dir: Path | str | None = None): + if policies_dir is None: + settings = get_settings() + policies_dir = getattr(settings, "POLICIES_DIR", "/app/policies") + self.policies_dir = Path(policies_dir) + + def get_benchmark_path(self, framework: str, slug: str, version: str) -> Path: + """Get the path to a benchmark's directory.""" + return self.policies_dir / framework / slug / version + + def get_benchmark_metadata(self, framework: str, slug: str, version: str) -> dict[str, Any]: + """Read metadata.json for a benchmark. + + Args: + framework: e.g., "cis" + slug: e.g., "microsoft-365-foundations" + version: e.g., "v3.1.0" + + Returns: + The full metadata dict including benchmark info and controls. + + Raises: + FileNotFoundError: If metadata.json doesn't exist. + """ + path = self.get_benchmark_path(framework, slug, version) / "metadata.json" + if not path.exists(): + raise FileNotFoundError(f"Metadata not found: {path}") + return json.loads(path.read_text(encoding="utf-8")) + + def get_control_metadata( + self, framework: str, slug: str, version: str, control_id: str + ) -> dict[str, Any]: + """Get control details from metadata.json. + + Args: + framework: e.g., "cis" + slug: e.g., "microsoft-365-foundations" + version: e.g., "v3.1.0" + control_id: e.g., "CIS-1.1.1" + + Returns: + Control metadata dict. + + Raises: + ValueError: If control is not found. + FileNotFoundError: If metadata.json doesn't exist. + """ + metadata = self.get_benchmark_metadata(framework, slug, version) + for control in metadata.get("controls", []): + if control.get("control_id") == control_id: + return control + raise ValueError(f"Control {control_id} not found in {framework}/{slug}/{version}") + + def list_benchmarks(self) -> list[dict[str, Any]]: + """List all available benchmarks by scanning the policies directory. + + Returns: + List of benchmark metadata dicts. + """ + benchmarks = [] + if not self.policies_dir.exists(): + return benchmarks + + for framework_dir in self.policies_dir.iterdir(): + if not framework_dir.is_dir(): + continue + for benchmark_dir in framework_dir.iterdir(): + if not benchmark_dir.is_dir(): + continue + for version_dir in benchmark_dir.iterdir(): + if not version_dir.is_dir(): + continue + metadata_file = version_dir / "metadata.json" + if metadata_file.exists(): + try: + metadata = json.loads(metadata_file.read_text(encoding="utf-8")) + benchmarks.append(metadata) + except (json.JSONDecodeError, OSError): + pass + return benchmarks + + def list_controls(self, framework: str, slug: str, version: str) -> list[dict[str, Any]]: + """List all controls for a specific benchmark. + + Returns: + List of control metadata dicts. + """ + metadata = self.get_benchmark_metadata(framework, slug, version) + return metadata.get("controls", []) + + def benchmark_exists(self, framework: str, slug: str, version: str) -> bool: + """Check if a benchmark exists on disk.""" + path = self.get_benchmark_path(framework, slug, version) / "metadata.json" + return path.exists() + + +@lru_cache(maxsize=1) +def get_file_reader() -> BenchmarkFileReader: + """Get a cached file reader instance.""" + return BenchmarkFileReader() diff --git a/backend-api/app/services/celery_client.py b/backend-api/app/services/celery_client.py new file mode 100644 index 00000000..bb4ce644 --- /dev/null +++ b/backend-api/app/services/celery_client.py @@ -0,0 +1,57 @@ +"""Celery client for queueing tasks from the backend API.""" + +from celery import Celery +from celery.result import AsyncResult + +from app.core.config import get_settings + +settings = get_settings() + +# Create Celery app connected to the same broker as the worker +celery_app = Celery( + "autoaudit", + broker=settings.REDIS_URL, +) + +# Task routing +celery_app.conf.update( + task_serializer="json", + accept_content=["json"], + result_serializer="json", + task_default_queue="autoaudit", +) + + +def queue_scan(scan_id: int) -> AsyncResult: + """Queue a scan task for execution by the worker. + + Args: + scan_id: The scan ID to process + + Returns: + Celery AsyncResult for tracking task status + """ + return celery_app.send_task( + "worker.tasks.run_scan", + args=[scan_id], + queue="autoaudit", + ) + + +def get_task_status(task_id: str) -> dict: + """Get the status of a task. + + Args: + task_id: The Celery task ID + + Returns: + Dict with task status info + """ + result = AsyncResult(task_id, app=celery_app) + return { + "task_id": task_id, + "status": result.status, + "ready": result.ready(), + "successful": result.successful() if result.ready() else None, + "result": result.result if result.ready() else None, + } diff --git a/backend-api/app/services/encryption.py b/backend-api/app/services/encryption.py new file mode 100644 index 00000000..174028d9 --- /dev/null +++ b/backend-api/app/services/encryption.py @@ -0,0 +1,58 @@ +"""Encryption service for securing sensitive data at rest. + +Uses Fernet symmetric encryption. The encryption key must be a 32-byte +base64-encoded key, provided via the ENCRYPTION_KEY environment variable. + +Generate a key with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +""" + +from cryptography.fernet import Fernet, InvalidToken + +from app.core.config import get_settings + +_fernet: Fernet | None = None + + +def get_fernet() -> Fernet: + """Get or create the Fernet encryption instance.""" + global _fernet + if _fernet is None: + settings = get_settings() + if not settings.ENCRYPTION_KEY: + raise ValueError( + "ENCRYPTION_KEY environment variable is required. " + "Generate one with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"" + ) + _fernet = Fernet(settings.ENCRYPTION_KEY.encode()) + return _fernet + + +def encrypt(plaintext: str) -> str: + """Encrypt a string and return the ciphertext as a string. + + Args: + plaintext: The string to encrypt. + + Returns: + The encrypted string (base64-encoded). + """ + if not plaintext: + return "" + return get_fernet().encrypt(plaintext.encode()).decode() + + +def decrypt(ciphertext: str) -> str: + """Decrypt a ciphertext string and return the plaintext. + + Args: + ciphertext: The encrypted string (base64-encoded). + + Returns: + The decrypted plaintext string. + + Raises: + InvalidToken: If the ciphertext is invalid or the key is wrong. + """ + if not ciphertext: + return "" + return get_fernet().decrypt(ciphertext.encode()).decode() diff --git a/backend-api/app/services/evidence_validator.py b/backend-api/app/services/evidence_validator.py new file mode 100644 index 00000000..39462fea --- /dev/null +++ b/backend-api/app/services/evidence_validator.py @@ -0,0 +1,131 @@ +"""Evidence validator + +This module provides a lightweight validator pass that checks whether an uploaded +evidence artifact contains expected evidence signals for a given strategy. + +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Dict, List + + +# These terms are intentionally conservative and designed to evolve. +STRATEGY_REQUIRED_TERMS: dict[str, list[str]] = { + "CIS Microsoft 365 Audit": [ + "mfa", + "conditional access", + "legacy authentication", + "audit log", + "admin", + "defender", + ], + "NIST Compliance Check": [ + "incident response", + "access control", + "least privilege", + "logging", + "monitoring", + "backup", + ], + "ISO 27001 Assessment": [ + "isms", + "policy", + "risk assessment", + "asset inventory", + "access control", + "logging", + ], + "SOC 2 Readiness": [ + "access", + "mfa", + "logging", + "monitoring", + "change management", + "incident", + ], + "GDPR Compliance Scan": [ + "personal data", + "pii", + "data retention", + "encryption", + "data subject", + "consent", + ], +} + + +def _normalize_text(text: str) -> str: + """Normalize text for matching. + + - lowercases + - normalizes whitespace + - normalizes common dash characters + """ + if not text: + return "" + out = text.lower() + out = out.replace("\u2010", "-").replace("\u2011", "-").replace("\u2013", "-").replace("\u2014", "-") + out = re.sub(r"\s+", " ", out) + return out.strip() + + +def _count_term(text: str, term: str) -> int: + """Count occurrences of a term in normalized text (case-insensitive).""" + if not text or not term: + return 0 + + # For simple word-ish terms, prefer word boundaries to reduce false positives. + # For multi-word phrases or terms with spaces/symbols, fall back to escaped substring regex. + term_norm = _normalize_text(term) + if not term_norm: + return 0 + + is_simple_word = bool(re.fullmatch(r"[a-z0-9]+", term_norm)) + if is_simple_word: + pattern = rf"\b{re.escape(term_norm)}\b" + else: + pattern = re.escape(term_norm) + + return len(re.findall(pattern, text, flags=re.IGNORECASE)) + + +@dataclass(frozen=True) +class ValidatorSummary: + matchedCount: int + totalTerms: int + + +def validate_text(strategy_name: str, extracted_text: str) -> dict: + """Validate extracted text for expected evidence signals. + + Returns validator_simple schema: + { + "matched": [{"term": str, "count": int}, ...], + "missing": [str, ...], + "summary": {"matchedCount": int, "totalTerms": int} + } + """ + terms = STRATEGY_REQUIRED_TERMS.get(strategy_name, []) + normalized = _normalize_text(extracted_text) + + matched: List[Dict[str, int | str]] = [] + missing: List[str] = [] + + for term in terms: + count = _count_term(normalized, term) + if count > 0: + matched.append({"term": term, "count": count}) + else: + missing.append(term) + + summary = ValidatorSummary(matchedCount=len(matched), totalTerms=len(terms)) + + return { + "matched": matched, + "missing": missing, + "summary": {"matchedCount": summary.matchedCount, "totalTerms": summary.totalTerms}, + } + diff --git a/backend-api/app/services/job_store.py b/backend-api/app/services/job_store.py deleted file mode 100644 index 80d7afc0..00000000 --- a/backend-api/app/services/job_store.py +++ /dev/null @@ -1,22 +0,0 @@ -from uuid import uuid4 -from datetime import datetime - -jobs = {} - -def create_export_job(scan_id: str, fmt: str) -> str: - job_id = str(uuid4()) - jobs[job_id] = { - "job_id": job_id, - "scan_id": scan_id, - "format": fmt, - "status": "pending", - "created_at": datetime.utcnow().isoformat(), - "url": None - } - # Simulate immediate completion as a test - jobs[job_id]["status"] = "completed" - jobs[job_id]["url"] = f"http://localhost:3000/downloads/{scan_id}.{fmt}" - return job_id - -def get_job_status(job_id: str): - return jobs.get(job_id) diff --git a/backend-api/app/services/m365_graph.py b/backend-api/app/services/m365_graph.py new file mode 100644 index 00000000..139c99d0 --- /dev/null +++ b/backend-api/app/services/m365_graph.py @@ -0,0 +1,155 @@ +"""Microsoft 365 (Graph) connection helpers. + +This module validates app-only (client credentials) connectivity to a target tenant +and probes basic tenant details via Microsoft Graph. + +We intentionally keep error messages user-safe so API layers can surface them +directly in HTTP responses without leaking secrets. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from functools import partial + +import anyio +import httpx +from msal import ConfidentialClientApplication + +GRAPH_SCOPES = ["https://graph.microsoft.com/.default"] +GRAPH_ORG_ENDPOINT = "/v1.0/organization?$select=id,displayName,verifiedDomains" + + +@dataclass(frozen=True) +class TenantDetails: + """Normalized tenant details returned from Graph.""" + + tenant_display_name: str | None + default_domain: str | None + verified_domains: list[str] + + +class M365ConnectionError(Exception): + """Raised when M365 auth/probe fails in a user-displayable way.""" + + +def _first_line(value: str | None) -> str: + if not value: + return "Unknown error" + return value.splitlines()[0].strip() or "Unknown error" + + +def _acquire_token_sync(*, tenant_id: str, client_id: str, client_secret: str) -> dict: + """Acquire an app-only token for Microsoft Graph (sync; run in thread).""" + app = ConfidentialClientApplication( + client_id=client_id, + client_credential=client_secret, + authority=f"https://login.microsoftonline.com/{tenant_id}", + ) + return app.acquire_token_for_client(scopes=GRAPH_SCOPES) + + +async def acquire_graph_access_token( + *, tenant_id: str, client_id: str, client_secret: str +) -> str: + """Acquire an app-only access token for Microsoft Graph.""" + tenant_id = (tenant_id or "").strip() + client_id = (client_id or "").strip() + client_secret = (client_secret or "").strip() + + if not tenant_id or not client_id or not client_secret: + raise M365ConnectionError("Missing tenant_id, client_id, or client_secret") + + try: + result = await anyio.to_thread.run_sync( + partial( + _acquire_token_sync, + tenant_id=tenant_id, + client_id=client_id, + client_secret=client_secret, + ) + ) + except Exception as e: + # MSAL can raise (e.g.) ValueError for invalid tenant/authority formats. + raise M365ConnectionError(f"Authentication failed: {_first_line(str(e))}") + + token = result.get("access_token") + if token: + return token + + # Typical errors include AADSTS codes like: + # - AADSTS90002 (tenant not found) + # - AADSTS700016 (app not found) + # - AADSTS7000215 (invalid secret) + desc = _first_line(result.get("error_description") or result.get("error")) + raise M365ConnectionError(f"Authentication failed: {desc}") + + +async def probe_tenant_details(*, access_token: str) -> TenantDetails: + """Probe basic tenant details via Graph /organization.""" + base_url = "https://graph.microsoft.com" + headers = {"Authorization": f"Bearer {access_token}"} + + async with httpx.AsyncClient(timeout=15.0, base_url=base_url) as http: + resp = await http.get(GRAPH_ORG_ENDPOINT, headers=headers) + + if resp.status_code == 403: + raise M365ConnectionError( + "Authenticated, but Graph denied access to /organization. " + "Add Microsoft Graph Application permission 'Organization.Read.All' and grant admin consent." + ) + + if resp.status_code >= 400: + try: + payload = resp.json() + # Graph error shape: {"error": {"code": "...", "message": "..."}} + if isinstance(payload.get("error"), dict): + msg = payload["error"].get("message") + else: + msg = str(payload.get("error") or "") + except Exception: + msg = resp.text + raise M365ConnectionError(f"Graph probe failed: {_first_line(msg)}") + + data = resp.json() + orgs = data.get("value") or [] + if not orgs: + return TenantDetails( + tenant_display_name=None, + default_domain=None, + verified_domains=[], + ) + + org = orgs[0] or {} + display_name = org.get("displayName") + + verified = org.get("verifiedDomains") or [] + verified_domains = [d.get("name") for d in verified if isinstance(d, dict) and d.get("name")] + default_domain = next( + ( + d.get("name") + for d in verified + if isinstance(d, dict) and d.get("isDefault") and d.get("name") + ), + None, + ) + + return TenantDetails( + tenant_display_name=display_name, + default_domain=default_domain, + verified_domains=verified_domains, + ) + + +async def validate_m365_connection( + *, tenant_id: str, client_id: str, client_secret: str +) -> TenantDetails: + """Validate credentials by acquiring a token and probing tenant details.""" + token = await acquire_graph_access_token( + tenant_id=tenant_id, + client_id=client_id, + client_secret=client_secret, + ) + return await probe_tenant_details(access_token=token) + + diff --git a/backend-api/pyproject.toml b/backend-api/pyproject.toml index 4519e319..9897ff61 100644 --- a/backend-api/pyproject.toml +++ b/backend-api/pyproject.toml @@ -14,4 +14,29 @@ dependencies = [ "sqlalchemy>=2.0.0", "asyncpg>=0.29.0", "alembic>=1.13.0", + # OAuth helpers (Google SSO) + "httpx>=0.27.0", + "httpx-oauth>=0.15.0", + # Microsoft Graph (client credentials via MSAL) + "msal>=1.31.0", + # Multipart upload support for evidence/file endpoints + "python-multipart>=0.0.12", + # Encryption for credentials at rest + "cryptography>=42.0.0", + # Celery client for queueing tasks + "celery[redis]>=5.3.0", +] + +[project.optional-dependencies] +# Heavy OCR/reporting stack lives in the "evidence" extra so backend devs +# aren't forced to install it unless they touch evidence scanning. +evidence = [ + "pytesseract>=0.3.10", + "pillow>=10.4.0", + "opencv-python>=4.10.0.84", + "python-docx>=1.1.2", + "docx2pdf>=0.1.8", + "pymupdf>=1.24.10", + "fpdf2>=2.8.1", + "tabulate>=0.9.0", ] diff --git a/backend-api/uv.lock b/backend-api/uv.lock index 54172301..7e4d1c10 100644 --- a/backend-api/uv.lock +++ b/backend-api/uv.lock @@ -21,6 +21,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" }, ] +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -45,6 +57,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, ] +[[package]] +name = "appscript" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/52/2fa70edfd98f0058219ecc2e365a3ba7aabd42db14ff9d7f44bbdcc5400d/appscript-1.4.0.tar.gz", hash = "sha256:b2c6fc770bf822ea45529c7084bc0ee340e67ab260016b01d28e0449ec8723be", size = 295279, upload-time = "2025-10-08T07:56:39.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/f6/4f83b7110c147e2f2b111d344535db27d950c72f903870d4f169221632cd/appscript-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0306c09f55f799716fc2badd072b16538a797d2acbc3ea2adc55553293d18188", size = 99403, upload-time = "2025-10-08T07:56:29.046Z" }, + { url = "https://files.pythonhosted.org/packages/78/bd/70e4f9d5053a3962accde8f6d500a427b16307c29d9be5097a435b750d1b/appscript-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:de0b644f1b86f05d7526e5621bd2fcf36f077fc90fd328e63ba12dd3e267eb9e", size = 85449, upload-time = "2025-10-08T07:56:30.92Z" }, + { url = "https://files.pythonhosted.org/packages/3f/85/790d5b08d797fd63d64729d38367490c65f0373916a5a4c2522e97dc6f56/appscript-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:48d961f83be1de6aa9286d0cfae2060d3ded4302902873d34cd45ca2757c50b3", size = 99403, upload-time = "2025-10-08T07:56:31.671Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e1/64e7543ef7b935db7979994cf831ac1609fcc8c0e7fb6dfa9ac9c058790b/appscript-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ab3be221cf5e798fe1a1930b93c4b791c5cae4e5fd781cb9fb6e35fb7930274", size = 85452, upload-time = "2025-10-08T07:56:32.46Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/ac82464026fb5b9b63e3f08c935ac0e983ad7723b3959b9d0187f36eed82/appscript-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ddecaff061f829fcfe5b0452a272f9aa5ac1f61b3929fa18b6f0f100e8eaf306", size = 99623, upload-time = "2025-10-08T07:56:33.199Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/a18c2b519efa8f36d03c7e835748f13a2cef8179f6eb4e957d96e3d8c668/appscript-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8edab6b8def4862c9582e5d8f5f72c23a3749f2d059f80e4b5ae101a53805116", size = 85545, upload-time = "2025-10-08T07:56:34.27Z" }, + { url = "https://files.pythonhosted.org/packages/a3/04/d6f3889a9e281c5ae86913f427441c9b04fb1975e61548ad0525a73d6981/appscript-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7710db497d279d819f487dea686866b56c6c1e557befef4631b78553c523334c", size = 99618, upload-time = "2025-10-08T07:56:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/59/fd/2259a7c6996628cabd9c7d5cc340bb695af31f047de17d70872caa4d7963/appscript-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0396978b95e06c2cabba117cc172add1df188c51a1b80fc6648aad16890a84c8", size = 85551, upload-time = "2025-10-08T07:56:36.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/15/748adf302d8f1f8f975bb26c1b918d84f1d39bb6c4730e0b91f551297984/appscript-1.4.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1a5747425d2a5732e2854cab0d0dc893ef077cabd7d57f7ec4caea2ac313e19f", size = 99700, upload-time = "2025-10-08T07:56:37.313Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e3/03dc0f97eab839f72061342d69bd34424e89876ce4026509aab3d74d4f23/appscript-1.4.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5efce3302c00674b769b79938cc5f66f7791ef45c6419e850a5f1c8f9fcefcc1", size = 85610, upload-time = "2025-10-08T07:56:38.103Z" }, +] + [[package]] name = "argon2-cffi" version = "23.1.0" @@ -152,27 +185,60 @@ source = { virtual = "." } dependencies = [ { name = "alembic" }, { name = "asyncpg" }, + { name = "celery", extra = ["redis"] }, + { name = "cryptography" }, { name = "fastapi" }, { name = "fastapi-users", extra = ["sqlalchemy"] }, + { name = "httpx" }, + { name = "httpx-oauth" }, + { name = "msal" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-dotenv" }, + { name = "python-multipart" }, { name = "sqlalchemy" }, { name = "uvicorn" }, ] +[package.optional-dependencies] +evidence = [ + { name = "docx2pdf" }, + { name = "fpdf2" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "pymupdf" }, + { name = "pytesseract" }, + { name = "python-docx" }, + { name = "tabulate" }, +] + [package.metadata] requires-dist = [ { name = "alembic", specifier = ">=1.13.0" }, { name = "asyncpg", specifier = ">=0.29.0" }, + { name = "celery", extras = ["redis"], specifier = ">=5.3.0" }, + { name = "cryptography", specifier = ">=42.0.0" }, + { name = "docx2pdf", marker = "extra == 'evidence'", specifier = ">=0.1.8" }, { name = "fastapi", specifier = ">=0.116.1" }, { name = "fastapi-users", extras = ["sqlalchemy"], specifier = ">=13.0.0" }, + { name = "fpdf2", marker = "extra == 'evidence'", specifier = ">=2.8.1" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "httpx-oauth", specifier = ">=0.15.0" }, + { name = "msal", specifier = ">=1.31.0" }, + { name = "opencv-python", marker = "extra == 'evidence'", specifier = ">=4.10.0.84" }, + { name = "pillow", marker = "extra == 'evidence'", specifier = ">=10.4.0" }, { name = "pydantic", specifier = ">=2.11.7" }, { name = "pydantic-settings", specifier = ">=2.10.1" }, + { name = "pymupdf", marker = "extra == 'evidence'", specifier = ">=1.24.10" }, + { name = "pytesseract", marker = "extra == 'evidence'", specifier = ">=0.3.10" }, + { name = "python-docx", marker = "extra == 'evidence'", specifier = ">=1.1.2" }, { name = "python-dotenv", specifier = ">=1.1.1" }, + { name = "python-multipart", specifier = ">=0.0.12" }, { name = "sqlalchemy", specifier = ">=2.0.0" }, + { name = "tabulate", marker = "extra == 'evidence'", specifier = ">=0.9.0" }, { name = "uvicorn", specifier = ">=0.35.0" }, ] +provides-extras = ["evidence"] [[package]] name = "bcrypt" @@ -232,6 +298,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110, upload-time = "2025-02-28T01:24:05.896Z" }, ] +[[package]] +name = "billiard" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, +] + +[[package]] +name = "celery" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "exceptiongroup" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzlocal" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/5f/b681ae3c89290d2ea6562ea96b40f5af6f6fc5f7743e2cd1a19e47721548/celery-5.6.0.tar.gz", hash = "sha256:641405206042d52ae460e4e9751a2e31b06cf80ab836fcf92e0b9311d7ea8113", size = 1712522, upload-time = "2025-11-30T17:39:46.282Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/4e/53a125038d6a814491a0ae3457435c13cf8821eb602292cf9db37ce35f62/celery-5.6.0-py3-none-any.whl", hash = "sha256:33cf01477b175017fc8f22c5ee8a65157591043ba8ca78a443fe703aa910f581", size = 444561, upload-time = "2025-11-30T17:39:44.314Z" }, +] + +[package.optional-dependencies] +redis = [ + { name = "kombu", extra = ["redis"] }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + [[package]] name = "cffi" version = "2.0.0" @@ -314,6 +424,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + [[package]] name = "click" version = "8.2.1" @@ -326,6 +525,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -400,6 +636,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -409,6 +654,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] +[[package]] +name = "docx2pdf" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appscript", marker = "sys_platform == 'darwin'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/5d/112531fff53cf60513e14fa1707755c874d47880ec4de7b2235302ad19a0/docx2pdf-0.1.8.tar.gz", hash = "sha256:6d2c20f9ad36eec75f4da017dc7a97622946954a6124ca0b11772875fa86fbed", size = 6483, upload-time = "2021-12-11T16:56:36.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/4f/1155781308281e67f80b829738a29e5354e03664c62311f753056afc873b/docx2pdf-0.1.8-py3-none-any.whl", hash = "sha256:00be1401fd486640314e993423a0a1cbdbc21142186f68549d962d505b2e8a12", size = 6741, upload-time = "2021-12-11T16:56:35.163Z" }, +] + [[package]] name = "email-validator" version = "2.3.0" @@ -483,6 +742,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/08/9968963c1fb8c34627b7f1fbcdfe9438540f87dc7c9bfb59bb4fd19a4ecf/fastapi_users_db_sqlalchemy-7.0.0-py3-none-any.whl", hash = "sha256:5fceac018e7cfa69efc70834dd3035b3de7988eb4274154a0dbe8b14f5aa001e", size = 6891, upload-time = "2025-01-04T13:09:02.869Z" }, ] +[[package]] +name = "fonttools" +version = "4.61.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884, upload-time = "2025-11-28T17:05:49.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/f3/91bba2721fb173fc68e09d15b6ccf3ad4f83d127fbff579be7e5984888a6/fonttools-4.61.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dc25a4a9c1225653e4431a9413d0381b1c62317b0f543bdcec24e1991f612f33", size = 2850151, upload-time = "2025-11-28T17:04:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8c/a1691dec01038ac7e7bb3ab83300dcc5087b11d8f48640928c02a873eb92/fonttools-4.61.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b493c32d2555e9944ec1b911ea649ff8f01a649ad9cba6c118d6798e932b3f0", size = 2389769, upload-time = "2025-11-28T17:04:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dd/5bb369a44319d92ba25612511eb8ed2a6fa75239979e0388907525626902/fonttools-4.61.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad751319dc532a79bdf628b8439af167181b4210a0cd28a8935ca615d9fdd727", size = 4893189, upload-time = "2025-11-28T17:04:18.398Z" }, + { url = "https://files.pythonhosted.org/packages/5e/02/51373fa8846bd22bb54e5efb30a824b417b058083f775a194a432f21a45f/fonttools-4.61.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2de14557d113faa5fb519f7f29c3abe4d69c17fe6a5a2595cc8cda7338029219", size = 4854415, upload-time = "2025-11-28T17:04:20.421Z" }, + { url = "https://files.pythonhosted.org/packages/8b/64/9cdbbb804577a7e6191448851c57e6a36eb02aa4bf6a9668b528c968e44e/fonttools-4.61.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:59587bbe455dbdf75354a9dbca1697a35a8903e01fab4248d6b98a17032cee52", size = 4870927, upload-time = "2025-11-28T17:04:22.625Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/e40b22919dc96dc30a70b58fec609ab85112de950bdecfadf8dd478c5a88/fonttools-4.61.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46cb3d9279f758ac0cf671dc3482da877104b65682679f01b246515db03dbb72", size = 4988674, upload-time = "2025-11-28T17:04:24.675Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5c/e857349ce8aedb2451b9448282e86544b2b7f1c8b10ea0fe49b7cb369b72/fonttools-4.61.0-cp310-cp310-win32.whl", hash = "sha256:58b4f1b78dfbfe855bb8a6801b31b8cdcca0e2847ec769ad8e0b0b692832dd3b", size = 1497663, upload-time = "2025-11-28T17:04:26.598Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0c/62961d5fe6f764d6cbc387ef2c001f5f610808c7aded837409836c0b3e7c/fonttools-4.61.0-cp310-cp310-win_amd64.whl", hash = "sha256:68704a8bbe0b61976262b255e90cde593dc0fe3676542d9b4d846bad2a890a76", size = 1546143, upload-time = "2025-11-28T17:04:28.432Z" }, + { url = "https://files.pythonhosted.org/packages/fd/be/5aa89cdddf2863d8afbdc19eb8ec5d8d35d40eeeb8e6cf52c5ff1c2dbd33/fonttools-4.61.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", size = 2847553, upload-time = "2025-11-28T17:04:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3e/6ff643b07cead1236a534f51291ae2981721cf419135af5b740c002a66dd/fonttools-4.61.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", size = 2388298, upload-time = "2025-11-28T17:04:32.161Z" }, + { url = "https://files.pythonhosted.org/packages/c3/15/fca8dfbe7b482e6f240b1aad0ed7c6e2e75e7a28efa3d3a03b570617b5e5/fonttools-4.61.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", size = 5054133, upload-time = "2025-11-28T17:04:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a2/821c61c691b21fd09e07528a9a499cc2b075ac83ddb644aa16c9875a64bc/fonttools-4.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", size = 5031410, upload-time = "2025-11-28T17:04:36.141Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/8b16339e93d03c732c8a23edefe3061b17a5f9107ddc47a3215ecd054cac/fonttools-4.61.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", size = 5030005, upload-time = "2025-11-28T17:04:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/ac/eb/d4e150427bdaa147755239c931bbce829a88149ade5bfd8a327afe565567/fonttools-4.61.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", size = 5154026, upload-time = "2025-11-28T17:04:40.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5f/3dd00ce0dba6759943c707b1830af8c0bcf6f8f1a9fe46cb82e7ac2aaa74/fonttools-4.61.0-cp311-cp311-win32.whl", hash = "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", size = 2276035, upload-time = "2025-11-28T17:04:42.59Z" }, + { url = "https://files.pythonhosted.org/packages/4e/44/798c472f096ddf12955eddb98f4f7c906e7497695d04ce073ddf7161d134/fonttools-4.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", size = 2327290, upload-time = "2025-11-28T17:04:44.57Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930, upload-time = "2025-11-28T17:04:46.639Z" }, + { url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016, upload-time = "2025-11-28T17:04:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425, upload-time = "2025-11-28T17:04:50.482Z" }, + { url = "https://files.pythonhosted.org/packages/af/00/acf18c00f6c501bd6e05ee930f926186f8a8e268265407065688820f1c94/fonttools-4.61.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", size = 4999632, upload-time = "2025-11-28T17:04:52.508Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e0/19a2b86e54109b1d2ee8743c96a1d297238ae03243897bc5345c0365f34d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", size = 4939438, upload-time = "2025-11-28T17:04:54.437Z" }, + { url = "https://files.pythonhosted.org/packages/04/35/7b57a5f57d46286360355eff8d6b88c64ab6331107f37a273a71c803798d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", size = 5088960, upload-time = "2025-11-28T17:04:56.348Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/6c5023eb2e0fe5d1ababc7e221e44acd3ff668781489cc1937a6f83d620a/fonttools-4.61.0-cp312-cp312-win32.whl", hash = "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", size = 2264404, upload-time = "2025-11-28T17:04:58.149Z" }, + { url = "https://files.pythonhosted.org/packages/36/0b/63273128c7c5df19b1e4cd92e0a1e6ea5bb74a400c4905054c96ad60a675/fonttools-4.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", size = 2314427, upload-time = "2025-11-28T17:04:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/334f0d7f181e5473cfb757e1b60f4e60e7fc64f28d406e5d364a952718c0/fonttools-4.61.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba774b8cbd8754f54b8eb58124e8bd45f736b2743325ab1a5229698942b9b433", size = 2841801, upload-time = "2025-11-28T17:05:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/cc/63/97b9c78e1f79bc741d4efe6e51f13872d8edb2b36e1b9fb2bab0d4491bb7/fonttools-4.61.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c84b430616ed73ce46e9cafd0bf0800e366a3e02fb7e1ad7c1e214dbe3862b1f", size = 2379024, upload-time = "2025-11-28T17:05:03.668Z" }, + { url = "https://files.pythonhosted.org/packages/4e/80/c87bc524a90dbeb2a390eea23eae448286983da59b7e02c67fa0ca96a8c5/fonttools-4.61.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2b734d8391afe3c682320840c8191de9bd24e7eb85768dd4dc06ed1b63dbb1b", size = 4923706, upload-time = "2025-11-28T17:05:05.494Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/a3b0374811a1de8c3f9207ec88f61ad1bb96f938ed89babae26c065c2e46/fonttools-4.61.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5c5fff72bf31b0e558ed085e4fd7ed96eb85881404ecc39ed2a779e7cf724eb", size = 4979751, upload-time = "2025-11-28T17:05:07.665Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3b/30f63b4308b449091573285f9d27619563a84f399946bca3eadc9554afbe/fonttools-4.61.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14a290c5c93fcab76b7f451e6a4b7721b712d90b3b5ed6908f1abcf794e90d6d", size = 4921113, upload-time = "2025-11-28T17:05:09.551Z" }, + { url = "https://files.pythonhosted.org/packages/41/6c/58e6e9b7d9d8bf2d7010bd7bb493060b39b02a12d1cda64a8bfb116ce760/fonttools-4.61.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:13e3e20a5463bfeb77b3557d04b30bd6a96a6bb5c15c7b2e7908903e69d437a0", size = 5063183, upload-time = "2025-11-28T17:05:11.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e3/52c790ab2b07492df059947a1fd7778e105aac5848c0473029a4d20481a2/fonttools-4.61.0-cp313-cp313-win32.whl", hash = "sha256:6781e7a4bb010be1cd69a29927b0305c86b843395f2613bdabe115f7d6ea7f34", size = 2263159, upload-time = "2025-11-28T17:05:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1f/116013b200fbeba871046554d5d2a45fefa69a05c40e9cdfd0d4fff53edc/fonttools-4.61.0-cp313-cp313-win_amd64.whl", hash = "sha256:c53b47834ae41e8e4829171cc44fec0fdf125545a15f6da41776b926b9645a9a", size = 2313530, upload-time = "2025-11-28T17:05:14.848Z" }, + { url = "https://files.pythonhosted.org/packages/d3/99/59b1e25987787cb714aa9457cee4c9301b7c2153f0b673e2b8679d37669d/fonttools-4.61.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:96dfc9bc1f2302224e48e6ee37e656eddbab810b724b52e9d9c13a57a6abad01", size = 2841429, upload-time = "2025-11-28T17:05:16.671Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/4c1911d4332c8a144bb3b44416e274ccca0e297157c971ea1b3fbb855590/fonttools-4.61.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3b2065d94e5d63aafc2591c8b6ccbdb511001d9619f1bca8ad39b745ebeb5efa", size = 2378987, upload-time = "2025-11-28T17:05:18.69Z" }, + { url = "https://files.pythonhosted.org/packages/24/b0/f442e90fde5d2af2ae0cb54008ab6411edc557ee33b824e13e1d04925ac9/fonttools-4.61.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e0d87e81e4d869549585ba0beb3f033718501c1095004f5e6aef598d13ebc216", size = 4873270, upload-time = "2025-11-28T17:05:20.625Z" }, + { url = "https://files.pythonhosted.org/packages/bb/04/f5d5990e33053c8a59b90b1d7e10ad9b97a73f42c745304da0e709635fab/fonttools-4.61.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cfa2eb9bae650e58f0e8ad53c49d19a844d6034d6b259f30f197238abc1ccee", size = 4968270, upload-time = "2025-11-28T17:05:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/94/9f/2091402e0d27c9c8c4bab5de0e5cd146d9609a2d7d1c666bbb75c0011c1a/fonttools-4.61.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4238120002e68296d55e091411c09eab94e111c8ce64716d17df53fd0eb3bb3d", size = 4919799, upload-time = "2025-11-28T17:05:24.437Z" }, + { url = "https://files.pythonhosted.org/packages/a8/72/86adab22fde710b829f8ffbc8f264df01928e5b7a8f6177fa29979ebf256/fonttools-4.61.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6ceac262cc62bec01b3bb59abccf41b24ef6580869e306a4e88b7e56bb4bdda", size = 5030966, upload-time = "2025-11-28T17:05:26.115Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a7/7c8e31b003349e845b853f5e0a67b95ff6b052fa4f5224f8b72624f5ac69/fonttools-4.61.0-cp314-cp314-win32.whl", hash = "sha256:adbb4ecee1a779469a77377bbe490565effe8fce6fb2e6f95f064de58f8bac85", size = 2267243, upload-time = "2025-11-28T17:05:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/20/ee/f434fe7749360497c52b7dcbcfdbccdaab0a71c59f19d572576066717122/fonttools-4.61.0-cp314-cp314-win_amd64.whl", hash = "sha256:02bdf8e04d1a70476564b8640380f04bb4ac74edc1fc71f1bacb840b3e398ee9", size = 2318822, upload-time = "2025-11-28T17:05:29.882Z" }, + { url = "https://files.pythonhosted.org/packages/33/b3/c16255320255e5c1863ca2b2599bb61a46e2f566db0bbb9948615a8fe692/fonttools-4.61.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:627216062d90ab0d98215176d8b9562c4dd5b61271d35f130bcd30f6a8aaa33a", size = 2924917, upload-time = "2025-11-28T17:05:31.46Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b8/08067ae21de705a817777c02ef36ab0b953cbe91d8adf134f9c2da75ed6d/fonttools-4.61.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b446623c9cd5f14a59493818eaa80255eec2468c27d2c01b56e05357c263195", size = 2413576, upload-time = "2025-11-28T17:05:33.343Z" }, + { url = "https://files.pythonhosted.org/packages/42/f1/96ff43f92addce2356780fdc203f2966206f3d22ea20e242c27826fd7442/fonttools-4.61.0-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:70e2a0c0182ee75e493ef33061bfebf140ea57e035481d2f95aa03b66c7a0e05", size = 4877447, upload-time = "2025-11-28T17:05:35.278Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1e/a3d8e51ed9ccfd7385e239ae374b78d258a0fb82d82cab99160a014a45d1/fonttools-4.61.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9064b0f55b947e929ac669af5311ab1f26f750214db6dd9a0c97e091e918f486", size = 5095681, upload-time = "2025-11-28T17:05:37.142Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f6/d256bd6c1065c146a0bdddf1c62f542e08ae5b3405dbf3fcc52be272f674/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cb5e45a824ce14b90510024d0d39dae51bd4fbb54c42a9334ea8c8cf4d95cbe", size = 4974140, upload-time = "2025-11-28T17:05:39.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0c/96633eb4b26f138cc48561c6e0c44b4ea48acea56b20b507d6b14f8e80ce/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e5ca8c62efdec7972dfdfd454415c4db49b89aeaefaaacada432f3b7eea9866", size = 5001741, upload-time = "2025-11-28T17:05:41.424Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/3b536bad3be4f26186f296e749ff17bad3e6d57232c104d752d24b2e265b/fonttools-4.61.0-cp314-cp314t-win32.whl", hash = "sha256:63c7125d31abe3e61d7bb917329b5543c5b3448db95f24081a13aaf064360fc8", size = 2330707, upload-time = "2025-11-28T17:05:43.548Z" }, + { url = "https://files.pythonhosted.org/packages/18/ea/e6b9ac610451ee9f04477c311ad126de971f6112cb579fa391d2a8edb00b/fonttools-4.61.0-cp314-cp314t-win_amd64.whl", hash = "sha256:67d841aa272be5500de7f447c40d1d8452783af33b4c3599899319f6ef9ad3c1", size = 2395950, upload-time = "2025-11-28T17:05:45.638Z" }, + { url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485, upload-time = "2025-11-28T17:05:47.573Z" }, +] + +[[package]] +name = "fpdf2" +version = "2.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "fonttools" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/c0/784b130a28f4ed612e9aff26d1118e1f91005713dcd0a35e60b54d316b56/fpdf2-2.8.5.tar.gz", hash = "sha256:af4491ef2e0a5fe476f9d61362925658949c995f7e804438c0e81008f1550247", size = 336046, upload-time = "2025-10-29T14:17:59.569Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a7/8532d8fffe6d1c388ad4941d678dd0da4d8da80434f2dbf4f35de0fa8029/fpdf2-2.8.5-py3-none-any.whl", hash = "sha256:2356b94e2a5fcbd1fe53ac5cbb83494e9003308860ab180050255ba50961d913", size = 301627, upload-time = "2025-10-29T14:17:57.685Z" }, +] + [[package]] name = "greenlet" version = "3.2.4" @@ -553,6 +883,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-oauth" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/07/db4ad128da3926be22eec586aa87dafd8840c9eb03fe88505fbed016b5c6/httpx_oauth-0.16.1.tar.gz", hash = "sha256:7402f061f860abc092ea4f5c90acfc576a40bbb79633c1d2920f1ca282c296ee", size = 44148, upload-time = "2024-12-20T07:23:02.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/4b/2b81e876abf77b4af3372aff731f4f6722840ebc7dcfd85778eaba271733/httpx_oauth-0.16.1-py3-none-any.whl", hash = "sha256:2fcad82f80f28d0473a0fc4b4eda223dc952050af7e3a8c8781342d850f09fb5", size = 38056, upload-time = "2024-12-20T07:23:00.394Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -562,6 +932,150 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "kombu" +version = "5.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/05/749ada8e51718445d915af13f1d18bc4333848e8faa0cb234028a3328ec8/kombu-5.6.1.tar.gz", hash = "sha256:90f1febb57ad4f53ca327a87598191b2520e0c793c75ea3b88d98e3b111282e4", size = 471548, upload-time = "2025-11-25T11:07:33.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/d6/943cf84117cd9ddecf6e1707a3f712a49fc64abdb8ac31b19132871af1dd/kombu-5.6.1-py3-none-any.whl", hash = "sha256:b69e3f5527ec32fc5196028a36376501682973e9620d6175d1c3d4eaf7e95409", size = 214141, upload-time = "2025-11-25T11:07:31.54Z" }, +] + +[package.optional-dependencies] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + [[package]] name = "makefun" version = "1.16.0" @@ -668,6 +1182,218 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "msal" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987, upload-time = "2025-09-22T23:05:47.294Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.12.0.88" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/71/25c98e634b6bdeca4727c7f6d6927b056080668c5008ad3c8fc9e7f8f6ec/opencv-python-4.12.0.88.tar.gz", hash = "sha256:8b738389cede219405f6f3880b851efa3415ccd674752219377353f017d2994d", size = 95373294, upload-time = "2025-07-07T09:20:52.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/68/3da40142e7c21e9b1d4e7ddd6c58738feb013203e6e4b803d62cdd9eb96b/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:f9a1f08883257b95a5764bf517a32d75aec325319c8ed0f89739a57fae9e92a5", size = 37877727, upload-time = "2025-07-07T09:13:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/33/7c/042abe49f58d6ee7e1028eefc3334d98ca69b030e3b567fe245a2b28ea6f/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:812eb116ad2b4de43ee116fcd8991c3a687f099ada0b04e68f64899c09448e81", size = 57326471, upload-time = "2025-07-07T09:13:41.26Z" }, + { url = "https://files.pythonhosted.org/packages/62/3a/440bd64736cf8116f01f3b7f9f2e111afb2e02beb2ccc08a6458114a6b5d/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:51fd981c7df6af3e8f70b1556696b05224c4e6b6777bdd2a46b3d4fb09de1a92", size = 45887139, upload-time = "2025-07-07T09:13:50.761Z" }, + { url = "https://files.pythonhosted.org/packages/68/1f/795e7f4aa2eacc59afa4fb61a2e35e510d06414dd5a802b51a012d691b37/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:092c16da4c5a163a818f120c22c5e4a2f96e0db4f24e659c701f1fe629a690f9", size = 67041680, upload-time = "2025-07-07T09:14:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/02/96/213fea371d3cb2f1d537612a105792aa0a6659fb2665b22cad709a75bd94/opencv_python-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:ff554d3f725b39878ac6a2e1fa232ec509c36130927afc18a1719ebf4fbf4357", size = 30284131, upload-time = "2025-07-07T09:14:08.819Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/eb88edc2e2b11cd2dd2e56f1c80b5784d11d6e6b7f04a1145df64df40065/opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2", size = 39000307, upload-time = "2025-07-07T09:14:16.641Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/08/26e68b6b5da219c2a2cb7b563af008b53bb8e6b6fcb3fa40715fcdb2523a/pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b", size = 5289809, upload-time = "2025-10-15T18:21:27.791Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/4e58fb097fb74c7b4758a680aacd558810a417d1edaa7000142976ef9d2f/pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1", size = 4650606, upload-time = "2025-10-15T18:21:29.823Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/1fa492aa9f77b3bc6d471c468e62bfea1823056bf7e5e4f1914d7ab2565e/pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363", size = 6221023, upload-time = "2025-10-15T18:21:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/c1/09/4de7cd03e33734ccd0c876f0251401f1314e819cbfd89a0fcb6e77927cc6/pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca", size = 8024937, upload-time = "2025-10-15T18:21:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/2e/69/0688e7c1390666592876d9d474f5e135abb4acb39dcb583c4dc5490f1aff/pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e", size = 6334139, upload-time = "2025-10-15T18:21:35.395Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/880921e98f525b9b44ce747ad1ea8f73fd7e992bafe3ca5e5644bf433dea/pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782", size = 7026074, upload-time = "2025-10-15T18:21:37.219Z" }, + { url = "https://files.pythonhosted.org/packages/28/03/96f718331b19b355610ef4ebdbbde3557c726513030665071fd025745671/pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10", size = 6448852, upload-time = "2025-10-15T18:21:39.168Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a0/6a193b3f0cc9437b122978d2c5cbce59510ccf9a5b48825096ed7472da2f/pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa", size = 7117058, upload-time = "2025-10-15T18:21:40.997Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c4/043192375eaa4463254e8e61f0e2ec9a846b983929a8d0a7122e0a6d6fff/pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275", size = 6295431, upload-time = "2025-10-15T18:21:42.518Z" }, + { url = "https://files.pythonhosted.org/packages/92/c6/c2f2fc7e56301c21827e689bb8b0b465f1b52878b57471a070678c0c33cd/pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d", size = 7000412, upload-time = "2025-10-15T18:21:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d2/5f675067ba82da7a1c238a73b32e3fd78d67f9d9f80fbadd33a40b9c0481/pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7", size = 2435903, upload-time = "2025-10-15T18:21:46.29Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + [[package]] name = "pwdlib" version = "0.2.1" @@ -824,6 +1550,58 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymupdf" +version = "1.26.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/a6f0e03a117fa2ad79c4b898203bb212b17804f92558a6a339298faca7bb/pymupdf-1.26.6.tar.gz", hash = "sha256:a2b4531cd4ab36d6f1f794bb6d3c33b49bda22f36d58bb1f3e81cbc10183bd2b", size = 84322494, upload-time = "2025-11-05T15:20:46.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/5c/dec354eee5fe4966c715f33818ed4193e0e6c986cf8484de35b6c167fb8e/pymupdf-1.26.6-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e46f320a136ad55e5219e8f0f4061bdf3e4c12b126d2740d5a49f73fae7ea176", size = 23178988, upload-time = "2025-11-05T14:31:19.834Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a0/11adb742d18142bd623556cd3b5d64649816decc5eafd30efc9498657e76/pymupdf-1.26.6-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:6844cd2396553c0fa06de4869d5d5ecb1260e6fc3b9d85abe8fa35f14dd9d688", size = 22469764, upload-time = "2025-11-05T14:32:34.654Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c8/377cf20e31f58d4c243bfcf2d3cb7466d5b97003b10b9f1161f11eb4a994/pymupdf-1.26.6-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:617ba69e02c44f0da1c0e039ea4a26cf630849fd570e169c71daeb8ac52a81d6", size = 23502227, upload-time = "2025-11-06T11:03:56.934Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/6e02e3d84b32c137c71a0a3dcdba8f2f6e9950619a3bc272245c7c06a051/pymupdf-1.26.6-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7777d0b7124c2ebc94849536b6a1fb85d158df3b9d873935e63036559391534c", size = 24115381, upload-time = "2025-11-05T14:33:54.338Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9d/30f7fcb3776bfedde66c06297960debe4883b1667294a1ee9426c942e94d/pymupdf-1.26.6-cp310-abi3-win32.whl", hash = "sha256:8f3ef05befc90ca6bb0f12983200a7048d5bff3e1c1edef1bb3de60b32cb5274", size = 17203613, upload-time = "2025-11-05T17:19:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e8/989f4eaa369c7166dc24f0eaa3023f13788c40ff1b96701f7047421554a8/pymupdf-1.26.6-cp310-abi3-win_amd64.whl", hash = "sha256:ce02ca96ed0d1acfd00331a4d41a34c98584d034155b06fd4ec0f051718de7ba", size = 18405680, upload-time = "2025-11-05T14:34:48.672Z" }, +] + +[[package]] +name = "pytesseract" +version = "0.3.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a6/7d679b83c285974a7cb94d739b461fa7e7a9b17a3abfd7bf6cbc5c2394b0/pytesseract-0.3.13.tar.gz", hash = "sha256:4bf5f880c99406f52a3cfc2633e42d9dc67615e69d8a509d74867d3baddb5db9", size = 17689, upload-time = "2024-08-16T02:33:56.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/33/8312d7ce74670c9d39a532b2c246a853861120486be9443eebf048043637/pytesseract-0.3.13-py3-none-any.whl", hash = "sha256:7a99c6c2ac598360693d83a416e36e0b33a67638bb9d77fdcac094a3589d4b34", size = 14705, upload-time = "2024-08-16T02:36:10.09Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.1" @@ -842,6 +1620,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "redis" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -914,6 +1750,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" }, ] +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + [[package]] name = "tomli" version = "2.3.0" @@ -963,6 +1808,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -984,6 +1841,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + [[package]] name = "uvicorn" version = "0.35.0" @@ -997,3 +1884,21 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8 wheels = [ { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, ] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] diff --git a/docker-compose.yml b/docker-compose.yml index 43d1ad3d..67c0de76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,23 @@ # AutoAudit Development Docker Compose # # Usage with profiles: -# docker compose up # Start only db and redis -# docker compose --profile frontend-dev up # For frontend devs (starts backend-api, db, redis) -# docker compose --profile backend-dev up # For backend devs (starts frontend, db, redis) +# docker compose up # Start infrastructure only (db, redis, opa) +# docker compose --profile frontend-dev up # For frontend devs (backend-api + infrastructure, run worker locally) +# docker compose --profile backend-dev up # For backend devs (frontend + infrastructure) +# docker compose --profile worker up # Add worker to any profile combination +# docker compose --profile powershell up # Add PowerShell service for Exchange/Teams cmdlets # docker compose --profile all up # Start all services # -# To run in detached mode, add -d flag: -# docker compose --profile frontend-dev up -d +# Common combinations: +# docker compose --profile frontend-dev up -d # Backend API + infra (worker runs locally) +# docker compose --profile frontend-dev --profile worker up -d # Backend API + worker + infra +# docker compose --profile frontend-dev --profile powershell up -d # Backend API + PowerShell service +# +# To run worker locally (for PowerShell/Exchange controls that need Docker access): +# cd engine && uv run celery -A worker.celery_app worker --loglevel=info +# +# To test collectors with PowerShell service: +# cd engine && uv run python -m scripts.test_collector -c exchange.organization.organization_config --use-service http://localhost:8001 services: # ============================================================================= @@ -46,6 +56,21 @@ services: retries: 5 restart: unless-stopped + opa: + image: openpolicyagent/opa:latest-debug + container_name: autoaudit-opa + command: ["run", "--server", "--addr=0.0.0.0:8181", "--log-level=info", "/policies"] + ports: + - "8181:8181" + volumes: + - ./engine/policies:/policies:ro + healthcheck: + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8181/health"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + # ============================================================================= # Application Services (profile-controlled) # ============================================================================= @@ -53,14 +78,43 @@ services: backend-api: profiles: ["frontend-dev", "all"] build: - context: ./backend-api - dockerfile: Dockerfile + context: . + dockerfile: backend-api/Dockerfile container_name: autoaudit-backend-api environment: + # Application environment - APP_ENV=dev + + # Required: Database connection (PostgreSQL with asyncpg driver) - DATABASE_URL=postgresql+asyncpg://autoaudit:autoaudit_dev_password@db:5432/autoaudit + + # Required: JWT signing key (CHANGE IN PRODUCTION) - SECRET_KEY=dev-secret-key-change-in-production + + # Public URLs (used for OAuth redirects) + - BACKEND_PUBLIC_URL=http://localhost:8000 + - FRONTEND_URL=http://localhost:3000 + + # Google OAuth (SSO) + - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-} + - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET:-} + + # Required: Redis URL for Celery task queue - REDIS_URL=redis://redis:6379 + + # Required: OPA server URL for policy evaluation + - OPA_URL=http://opa:8181 + + # Required: Fernet encryption key for M365 credentials at rest + # Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" + # This will be passed in with a secret provider in production. This is for local use only. + - ENCRYPTION_KEY=Ps-HiS3ww5QzQPc_Mdu5-JyA_jCNbdFHMdiwWSlAfgM= + + # Optional: Path to policies directory (default: /app/policies) + - POLICIES_DIR=/app/policies + volumes: + # Mount policies directory for benchmark/control discovery API + - ./engine/policies:/app/policies:ro ports: - "8000:8000" depends_on: @@ -68,6 +122,42 @@ services: condition: service_healthy redis: condition: service_healthy + opa: + condition: service_healthy + restart: unless-stopped + + worker: + profiles: ["worker", "all"] + build: + context: ./engine + dockerfile: Dockerfile + container_name: autoaudit-worker + environment: + # Required: Database connection (PostgreSQL - worker uses sync driver) + - DATABASE_URL=postgresql+asyncpg://autoaudit:autoaudit_dev_password@db:5432/autoaudit + + # Required: Redis URL for Celery broker + - REDIS_URL=redis://redis:6379 + + # Required: OPA server URL for policy evaluation + - OPA_URL=http://opa:8181 + + # Required: Must match ENCRYPTION_KEY in backend-api for credential decryption + - ENCRYPTION_KEY=Ps-HiS3ww5QzQPc_Mdu5-JyA_jCNbdFHMdiwWSlAfgM= + + # Optional: PowerShell service URL for Exchange/Teams cmdlets + # When set, worker uses HTTP service instead of spawning Docker containers + - POWERSHELL_SERVICE_URL=http://powershell-service:8001 + volumes: + - ./engine:/app/engine:ro + - ./engine/policies:/app/policies:ro + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + opa: + condition: service_healthy restart: unless-stopped frontend: @@ -77,11 +167,26 @@ services: dockerfile: Dockerfile container_name: autoaudit-frontend environment: - - REACT_APP_API_URL=http://localhost:8000 + - VITE_API_URL=http://localhost:8000 ports: - "3000:3000" restart: unless-stopped + powershell-service: + profiles: ["powershell", "all"] + build: + context: ./engine/powershell + dockerfile: Dockerfile + container_name: autoaudit-powershell-service + ports: + - "8001:8001" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8001/health"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + volumes: postgres_data: redis_data: diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..bb4dc6f3 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,336 @@ +# Contributing to AutoAudit + +This guide helps you find the right area to contribute based on your skills and interests. AutoAudit is a monorepo with several modules, and you don't necessarily need to understand the whole system to make meaningful contributions. + +## Project Overview + +AutoAudit is a compliance automation platform for cloud environments. It collects configuration data from cloud services, evaluates it against compliance frameworks (like CIS benchmarks), and presents findings through a dashboard. The platform is built around these core components: + +- A REST API that handles authentication and orchestrates scans +- A scanning engine that collects data and evaluates compliance policies +- A frontend dashboard for viewing results +- Infrastructure for deployment and monitoring + +## Module Guide + +### Backend API + +**Location**: `/backend-api` + +**What it does**: The backend API is the central hub of the application. It handles user authentication, manages tenants and scans, serves compliance results to the frontend, and coordinates with the scanning engine. + +**Tech stack**: +- Python 3.10+ +- FastAPI with Pydantic for request/response validation +- SQLAlchemy 2.0+ with async PostgreSQL +- FastAPI-Users for JWT authentication +- Alembic for database migrations + +**Good fit if you**: +- Have Python experience and want to work on web APIs +- Are interested in authentication systems and RBAC +- Want to learn FastAPI and modern Python async patterns +- Enjoy designing database schemas and writing migrations + +**Key directories**: +- `app/api/v1/` - API endpoint handlers +- `app/models/` - SQLAlchemy database models +- `app/schemas/` - Pydantic request/response schemas +- `app/services/` - Business logic layer +- `app/core/` - Configuration, auth, and middleware + +**Getting started**: See `/backend-api/README.md` for setup instructions and examples of securing endpoints. + +--- + +### Frontend + +**Location**: `/frontend` + +**What it does**: The frontend is a React dashboard that displays compliance scan results, visualizes data through charts, and provides the user interface for managing scans and settings. + +**Tech stack**: +- React 19 +- Tailwind CSS for styling +- React Router for navigation +- Chart.js for data visualization + +**Good fit if you**: +- Have JavaScript/TypeScript and React experience +- Are interested in data visualization and dashboards +- Want to improve UX and design user-friendly interfaces +- Enjoy working with component libraries and styling systems + +**Key directories**: +- `src/components/` - Reusable UI components +- `src/pages/` - Route-level page components +- `src/api/` - API client and data fetching +- `src/context/` - Shared app state (e.g. auth) + +**Note**: The frontend currently uses mock data in some areas. Connecting it to the live backend API is ongoing work. + +--- + +### Engine + +**Location**: `/engine` + +**What it does**: The engine is the compliance brain of the platform. It contains data collectors that fetch configuration from cloud APIs, and Rego policies that evaluate whether that configuration meets compliance standards. + +**Tech stack**: +- Python for data collectors +- Open Policy Agent (OPA) for policy evaluation +- Rego policy language +- Microsoft Graph API client + +**Good fit if you**: +- Are interested in cloud security and compliance +- Want to learn policy-as-code with OPA and Rego +- Have experience with cloud platforms (Azure, M365, GCP) +- Enjoy working with APIs and data transformation + +**Key directories**: +- `collectors/` - Data collectors for cloud APIs (see `/engine/collectors/README.md`) +- `policies/` - Rego policies organized by framework (see `/engine/policies/README.md`) + +**Getting started**: The engine has detailed documentation for writing new collectors and policies. Start with the READMEs in the `/engine/collectors/` and `/engine/policies/` directories. + +--- + +### When You Complete a Component + +This section explains what you need to update when you finish work on different parts of the compliance engine. Following these steps ensures your work is properly integrated and can be used in scans. + +#### Completing a Collector + +When you finish implementing a data collector: + +1. **Register the collector** in `engine/collectors/registry.py`: + ```python + from collectors.entra.my_domain.my_collector import MyCollector + + DATA_COLLECTORS: dict[str, type[BaseDataCollector]] = { + # ... existing collectors ... + "entra.my_domain.my_collector": MyCollector, + } + ``` + +2. **Update metadata.json** for the benchmark version you're implementing: + - Set `data_collector_id` to your collector's registry ID + - Change `automation_status` from `not_started` to `ready` (or `deferred`/`blocked` with notes if applicable) + + File: `engine/policies/cis/microsoft-365-foundations/vX.X.X/metadata.json` + +3. **Update the controls documentation**: + - Set the Status column to "Automated" or "Deferred" + - Add the Collector ID + + File: `docs/engine/policies/cis/microsoft-365-foundations/vX.X.X/controls.md` + +4. **Document your collector output** in the analysis document: + - Add example JSON output + - Note what the OPA policy can evaluate + + File: `docs/engine/policies/cis/microsoft-365-foundations/vX.X.X/controls.md` + +5. **Test your collector** against a live M365 tenant (see Testing Your Collector below) + +#### Testing Your Collector + +Use the collector test script to verify your collector works against a live M365 tenant. + +**Step 1: Set environment variables** + +You need to configure credentials for your M365 app registration before running the test script. + +**macOS / Linux:** +```bash +export M365_TENANT_ID=your-tenant-id-here +export M365_CLIENT_ID=your-client-id-here +export M365_CLIENT_SECRET=your-client-secret-here +``` + +**Windows (PowerShell):** +```powershell +$env:M365_TENANT_ID = "your-tenant-id-here" +$env:M365_CLIENT_ID = "your-client-id-here" +$env:M365_CLIENT_SECRET = "your-client-secret-here" +``` + +**Step 2: Run the test script** + +**macOS / Linux:** +```bash +cd engine + +# List available collectors +python -m scripts.test_collector --list + +# Test a specific collector +python -m scripts.test_collector -c entra.roles.cloud_only_admins + +# Test and save output to a file +python -m scripts.test_collector -c entra.roles.cloud_only_admins -o ./samples/ + +# Test all collectors and generate summary report +python -m scripts.test_collector --all -o ./samples/ +``` + +**Windows (PowerShell):** +```powershell +cd engine + +# List available collectors +python -m scripts.test_collector --list + +# Test a specific collector +python -m scripts.test_collector -c entra.roles.cloud_only_admins + +# Test and save output to a file +python -m scripts.test_collector -c entra.roles.cloud_only_admins -o .\samples\ + +# Test all collectors and generate summary report +python -m scripts.test_collector --all -o .\samples\ +``` + +**What the test script does:** +- Authenticates to your M365 tenant using the provided credentials +- Runs the specified collector and captures the JSON output +- Reports elapsed time and any errors encountered +- Optionally saves results to a JSON file for documentation + +#### Completing a Rego Policy + +When you finish implementing a Rego policy: + +1. **Ensure metadata annotations** are complete in the policy file: + ```rego + # METADATA + # title: Short control name + # description: What this control checks + # custom: + # control_id: X.X.X + # framework: cis + # benchmark: microsoft-365-foundations + # version: vX.X.X + # severity: critical|high|medium|low + # service: EntraID|Exchange|SharePoint|Teams|etc + # requires_permissions: + # - Permission.Read.All + ``` + +2. **Update metadata.json** for the benchmark version: + - Set `policy_file` to your Rego file name + + File: `engine/policies/cis/microsoft-365-foundations/vX.X.X/metadata.json` + +3. **Test the policy** with sample collector output using OPA eval. + +#### When Both Collector and Policy are Ready + +A control is scannable when both its collector and policy are implemented: + +1. Set `automation_status` to `ready` in metadata.json +2. Update the controls.md Status column to "Automated" +3. Verify the control works end-to-end with the test harness + +#### Control Metadata Schema + +Each control in metadata.json uses this schema: + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `control_id` | string | Yes | Control ID without framework prefix (e.g., "1.1.1") | +| `title` | string | Yes | Control title | +| `description` | string | No | Control description | +| `severity` | string | No | critical, high, medium, low | +| `service` | string | No | EntraID, Exchange, SharePoint, Teams, etc. | +| `level` | string | Yes | "L1" or "L2" | +| `is_manual` | boolean | Yes | true if no API available | +| `benchmark_audit_type` | string | Yes | What the benchmark says: "Automated" or "Manual" | +| `automation_status` | string | Yes | ready, deferred, blocked, manual, not_started | +| `data_collector_id` | string | Nullable | Collector registry ID, null for manual | +| `policy_file` | string | Nullable | Rego policy filename, null for manual | +| `requires_permissions` | array | Nullable | Required API permissions | +| `notes` | string | Nullable | Blockers, special considerations | + +--- + +### Infrastructure + +**Location**: `/infrastructure` + +**What it does**: Contains monitoring configuration, deployment infrastructure, and operational tooling for the platform. + +**NB:** AutoAudit currently doesn't have any configured infrastructure to deploy to, so the deployment part of this is conceptual at present. As a result, the monitoring and alerting is also conceptual. + +**Tech stack**: +- Docker and Docker Compose +- Monitoring tools +- CI/CD pipelines (GitHub Actions in `/.github/workflows`) + +**Good fit if you**: +- Have DevOps or platform engineering experience +- Are interested in monitoring, alerting, and observability +- Want to work on deployment automation and CI/CD +- Enjoy containerization and infrastructure-as-code + +**Key areas**: +- `monitoring/` - Monitoring and alerting configuration +- Root `docker-compose.yml` - Local development environment +- `/.github/workflows/` - CI/CD pipeline definitions + +--- + +## Git Workflow + +We use a simple branch-based workflow: + +1. **Create a feature branch from main** + ```bash + git checkout main + git pull origin main + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes and commit** + ```bash + git add . + git commit -m "Add description of your changes" + ``` + +3. **Push your branch** + ```bash + git push origin feature/your-feature-name + ``` + +4. **Open a Pull Request** against the `main` branch on GitHub + +### Branch naming + +Use descriptive branch names that indicate what you're working on: +- `feature/add-user-roles` - New functionality +- `hotfix/login-redirect-bug` - Bug fixes +- `docs/update-readme` - Documentation changes +- `refactor/cleanup-api-routes` - Code improvements + +## Pull Request Guidelines + +Good pull requests make the review process smoother for everyone: + +- **Keep them focused** - One PR should do one thing. If you find yourself writing "and" in the description, consider splitting it up. + +- **Write a clear description** - Explain what the change does and why. Include screenshots for UI changes. + +- **Test your changes** - Run tests locally before pushing. If you're adding new functionality, add tests for it. + +- **Update documentation** - If your change affects how something works, update the relevant docs. + +- **Respond to feedback** - Reviewers may request changes. Address them or explain why you disagree. + +## Where to Get Help + +- **Microsoft Planner items** - Check what's on the board, add tasks for things you're working on and keep them updated so the team is aware +- **Code Review** - Ask questions in your PR if you're unsure about something +- **Team Leads** - Reach out to the relevant module or team lead for guidance + diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 00000000..43bacd77 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,180 @@ +# Getting Started + +This guide will help you get the AutoAudit development environment running on your machine. By the end, you'll have the full stack running locally and be ready to start contributing. + +## Prerequisites + +Before you begin, make sure you have the following installed: + +- **Git** - For version control +- **Docker Desktop** - For running the development services +- **VS Code** (recommended) - Or your preferred editor + +Depending on which module you'll be working on, you may also need: + +- **Python 3.10+** and **uv** - For backend-api or engine work +- **Node.js 20+** and **npm** - For frontend work + +## Clone the Repository + +```bash +git clone https://github.com/Hardhat-Enterprises/AutoAudit.git +cd AutoAudit +``` + +## Quick Start with Docker + +The fastest way to get everything running is with Docker Compose. This will start the full stack including the database, Redis, OPA, and all application services. + +If you want to test **Google SSO** locally, create a `.env` file (gitignored) and set: + +- `GOOGLE_OAUTH_CLIENT_ID` +- `GOOGLE_OAUTH_CLIENT_SECRET` + +You can start from the template: + +```bash +cp env.example .env +``` + +```bash +docker compose --profile all up --build -d +``` + +Once the containers are running: + +- Frontend: http://localhost:3000 +- Backend API: http://localhost:8000 +- API Documentation: http://localhost:8000/docs +- OPA: http://localhost:8181 + +The backend automatically runs database migrations and seeds a default admin user on first startup: + +- Email: `admin@example.com` +- Password: `admin` + + +### Infrastructure Only (default) + +Starts the infrastructure services. Use this when you want to run both the frontend and backend locally. + +```bash +docker compose up -d +``` + +Services started: +- PostgreSQL on port 5432 +- Redis on port 6379 +- OPA on port 8181 + +### Frontend Development + +If you're working on the frontend and want the backend running in Docker: + +```bash +docker compose --profile frontend-dev up -d +cd frontend +npm install +npm start +``` + +This starts the backend-api in Docker (port 8000), and you run the frontend locally (port 3000). + +### Backend Development + +If you're working on the backend and want the frontend running in Docker: + +```bash +docker compose --profile backend-dev up -d +cd backend-api +uv sync +uv run uvicorn app.main:app --reload --port 8000 +``` + +This starts the frontend in Docker (port 3000), and you run the backend locally (port 8000). + +### Full Stack in Docker + +For testing or demos, run everything in containers: + +```bash +docker compose --profile all up -d +``` + +## Module-Specific Setup + +If you're developing a specific module locally, here's how to set each one up. + +### Backend API + +The backend is a FastAPI application using Python and the uv package manager. + +```bash +cd backend-api + +# Install dependencies +uv sync + +# Run database migrations +uv run alembic upgrade head + +# Seed default admin user (optional, dev only) +uv run python -m app.db.init_db + +# Start the development server with hot reload +uv run uvicorn app.main:app --reload --port 8000 +``` + +The API documentation will be available at http://localhost:8000/docs. + +Make sure PostgreSQL and Redis are running (via `docker compose up -d`) before starting the backend. + +### Frontend + +The frontend is a React application. + +```bash +cd frontend + +# Install dependencies +npm install + +# Start the development server +npm start +``` + +The app will open at http://localhost:3000. + +### Engine + +The engine runs as the `worker` (Celery) service when you start the full stack with Docker Compose: + +```bash +docker compose --profile all up --build -d +``` + +The worker logs are output directly to the Docker logs. You can view them with `docker compose logs -f worker`. + +## Verifying Your Setup + +Here's how to confirm everything is working: + +1. **Database**: Connect to PostgreSQL at `localhost:5432` (user: `autoaudit`, password: `autoaudit_dev_password`, database: `autoaudit`) + +2. **Backend API**: Visit http://localhost:8000/docs - you should see the Swagger UI + +3. **Frontend**: Visit http://localhost:3000 - you should see the dashboard + +4. **OPA**: Visit http://localhost:8181/health - you should get a healthy response + +## Common Issues + +**Port already in use**: Check if you have other services running on ports 3000, 5432, 6379, 8000, or 8181. Stop them or modify the ports in docker-compose.yml. + +**Docker containers won't start**: Run `docker compose logs` to see error messages. Often it's a port conflict or insufficient resources allocated to Docker. + +**Database connection errors**: Make sure the db container is healthy before starting the backend. Check with `docker compose ps`. + +## Next Steps + +Now that you're set up, head over to [CONTRIBUTING.md](./CONTRIBUTING.md) to learn about the different modules and find the right area to start contributing based on your skills and interests. \ No newline at end of file diff --git a/docs/engine/manual-collector-testing.md b/docs/engine/manual-collector-testing.md new file mode 100644 index 00000000..f10db5c3 --- /dev/null +++ b/docs/engine/manual-collector-testing.md @@ -0,0 +1,49 @@ +# Manual Collector Testing + +You can run collectors directly against a live M365 tenant to see what data they return. This is useful when developing OPA/Rego policies, since you'll need to know the exact JSON structure to write your policy rules against. + +## Before you start + +Make sure you've run `uv sync` from the `engine` directory to install dependencies. + +You'll also need access to an M365 test tenant with an app registration that has the appropriate Graph API permissions. + +## Set your environment variables + +The test script authenticates using three environment variables. + +On Windows (PowerShell): +```powershell +$env:M365_TENANT_ID="your-tenant-id" +$env:M365_CLIENT_ID="your-client-id" +$env:M365_CLIENT_SECRET="your-client-secret" +``` + +On macOS or Linux: +```bash +export M365_TENANT_ID="your-tenant-id" +export M365_CLIENT_ID="your-client-id" +export M365_CLIENT_SECRET="your-client-secret" +``` + +## Run a collector + +From the `engine` directory, run: + +```bash +uv run python -m scripts.test_collector -c entra.roles.cloud_only_admins +``` + +The JSON response will be printed to the console. + +To see what collectors are available, run: + +```bash +uv run python -m scripts.test_collector --list +``` + +If you want to save the output to a file instead, add the `-o` flag with a directory: + +```bash +uv run python -m scripts.test_collector -c entra.roles.cloud_only_admins -o ./samples/ +``` diff --git a/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md b/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md new file mode 100644 index 00000000..2c6f0ce6 --- /dev/null +++ b/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md @@ -0,0 +1,278 @@ +# CIS Microsoft 365 Foundations Benchmark v6.0.0 - Control Status + +This document provides a comprehensive overview of all 140 controls in the CIS Microsoft 365 Foundations Benchmark v6.0.0, including their automation status and implementation progress. + +*Last Updated: 2025-12-21* + +--- + +## Summary + +| Metric | Count | +|--------|-------| +| **Total Controls** | 140 | +| **CIS Automated** | 117 | +| **CIS Manual** | 23 | + +### Our Implementation Status + +| Our Audit Type | Count | Description | +|----------------|-------|-------------| +| Automated | 46 | Fully automatable with current collectors | +| Deferred | 12 | Collector works but needs "compliant with review" capability | +| Blocked | 21 | Collector exists but authentication issues prevent execution | +| Manual | 14 | No API available, truly requires manual verification | +| Not Started | 47 | No collector implemented yet | + +--- + +## Legend + +**Our Audit Type:** +- **Automated** - We can fully automate pass/fail determination +- **Deferred** - Collector works but requires user involvement to verify exclusions/gaps +- **Blocked** - Collector exists but authentication issues prevent execution +- **Manual** - No API available, truly requires manual verification + +**Status:** +- **Implemented** - Collector exists and is registered +- **Pending** - Collector exists but blocked/not registered +- **Not Started** - No collector implemented yet +- **N/A** - Truly manual, no collector needed + +--- + +## Section 1: Microsoft 365 Admin Center + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 1.1.1 | L1 | Ensure Administrative accounts are cloud-only | Automated | Automated | `entra.roles.cloud_only_admins` | Implemented | | +| 1.1.2 | L1 | Ensure two emergency access accounts have been defined | Manual | Manual | | N/A | Organizational policy; requires human designation of accounts | +| 1.1.3 | L1 | Ensure that between two and four global admins are designated | Automated | Automated | `entra.roles.privileged_roles` | Implemented | | +| 1.1.4 | L1 | Ensure administrative accounts use licenses with a reduced application footprint | Automated | Not Started | | Not Started | Need to check user license assignments | +| 1.2.1 | L2 | Ensure that only organizationally managed/approved public groups exist | Automated | Not Started | `entra.groups.groups` | Not Started | Collector exists but control logic not defined | +| 1.2.2 | L1 | Ensure sign-in to shared mailboxes is blocked | Automated | Not Started | `exchange.mailbox.mailboxes` | Not Started | Collector exists but control logic not defined | +| 1.3.1 | L1 | Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)' | Automated | Automated | `entra.domains.password_policy` | Implemented | | +| 1.3.2 | L2 | Ensure 'Idle session timeout' is set to '3 hours (or less)' for unmanaged devices | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires CA policy coverage verification | +| 1.3.3 | L2 | Ensure 'External sharing' of calendars is not available | Automated | Not Started | `exchange.organization.sharing_policy` | Not Started | Collector exists but control logic not defined | +| 1.3.4 | L1 | Ensure 'User owned apps and services' is restricted | Automated | Automated | `entra.applications.apps_and_services_settings` | Implemented | | +| 1.3.5 | L1 | Ensure internal phishing protection for Forms is enabled | Automated | Automated | `entra.applications.forms_settings` | Implemented | | +| 1.3.6 | L2 | Ensure the customer lockbox feature is enabled | Automated | Automated | `exchange.organization.organization_config` | Implemented | | +| 1.3.7 | L2 | Ensure 'third-party storage services' are restricted in 'Microsoft 365 on the web' | Automated | Not Started | `exchange.organization.owa_mailbox_policy` | Not Started | Collector exists but control logic not defined | +| 1.3.8 | L2 | Ensure that Sways cannot be shared with people outside of your organization | Manual | Manual | | N/A | No API available for Sway settings | +| 1.3.9 | L1 | Ensure shared bookings paged are restricted to select users | Automated | Not Started | | Not Started | Need Bookings API collector | + +--- + +## Section 2: Microsoft Defender for Office 365 + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 2.1.1 | L2 | Ensure Safe Links for Office Applications is Enabled | Automated | Automated | `exchange.protection.safe_links_policy` | Implemented | | +| 2.1.2 | L1 | Ensure the Common Attachment Types Filter is enabled | Automated | Automated | `exchange.protection.malware_filter_policy` | Implemented | | +| 2.1.3 | L1 | Ensure notifications for internal users sending malware is Enabled | Automated | Automated | `exchange.protection.malware_filter_policy` | Implemented | | +| 2.1.4 | L2 | Ensure Safe Attachments policy is enabled | Automated | Automated | `exchange.protection.safe_attachment_policy` | Implemented | | +| 2.1.5 | L2 | Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled | Automated | Automated | `exchange.protection.atp_policy_o365` | Implemented | | +| 2.1.6 | L1 | Ensure Exchange Online Spam Policies are set to notify administrators | Automated | Automated | `exchange.protection.hosted_outbound_spam_filter` | Implemented | | +| 2.1.7 | L2 | Ensure that an anti-phishing policy has been created | Automated | Automated | `exchange.protection.anti_phish_policy` | Implemented | | +| 2.1.8 | L1 | Ensure that SPF records are published for all Exchange Domains | Automated | Automated | `exchange.dns.dns_security_records` | Implemented | DNS lookup for SPF TXT records | +| 2.1.9 | L1 | Ensure that DKIM is enabled for all Exchange Online Domains | Automated | Automated | `exchange.authentication.dkim_signing_config` | Implemented | | +| 2.1.10 | L1 | Ensure DMARC Records for all Exchange Online domains are published | Automated | Automated | `exchange.dns.dns_security_records` | Implemented | DNS lookup for DMARC TXT records | +| 2.1.11 | L2 | Ensure comprehensive attachment filtering is applied | Automated | Automated | `exchange.protection.malware_filter_policy` | Implemented | | +| 2.1.12 | L1 | Ensure the connection filter IP allow list is not used | Automated | Automated | `exchange.protection.hosted_connection_filter` | Implemented | | +| 2.1.13 | L1 | Ensure the connection filter safe list is off | Automated | Automated | `exchange.protection.hosted_connection_filter` | Implemented | | +| 2.1.14 | L1 | Ensure inbound anti-spam policies do not contain allowed domains | Automated | Automated | `exchange.protection.hosted_content_filter` | Implemented | | +| 2.1.15 | L1 | Ensure outbound anti-spam message limits are in place | Automated | Automated | `exchange.protection.hosted_outbound_spam_filter` | Implemented | | +| 2.2.1 | L1 | Ensure emergency access account activity is monitored | Manual | Manual | | N/A | Organizational policy; requires defining which accounts to monitor | +| 2.4.1 | L1 | Ensure Priority account protection is enabled and configured | Automated | Not Started | | Not Started | Need Priority Account Protection collector | +| 2.4.2 | L1 | Ensure Priority accounts have 'Strict protection' presets applied | Automated | Not Started | | Not Started | Need Priority Account Protection collector | +| 2.4.3 | L2 | Ensure Microsoft Defender for Cloud Apps is enabled and configured | Manual | Manual | | N/A | MCAS configuration requires portal verification | +| 2.4.4 | L1 | Ensure Zero-hour auto purge for Microsoft Teams is on | Automated | Automated | `exchange.protection.teams_protection_policy` | Implemented | | + +--- + +## Section 3: Data Management + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 3.1.1 | L1 | Ensure Microsoft 365 audit log search is Enabled | Automated | Automated | `exchange.organization.organization_config` | Implemented | Check AuditDisabled = False | +| 3.2.1 | L1 | Ensure DLP policies are enabled | Automated | Blocked | | Pending | IPPSSession requires certificate auth | +| 3.2.2 | L1 | Ensure DLP policies are enabled for Microsoft Teams | Automated | Blocked | | Pending | IPPSSession requires certificate auth | +| 3.3.1 | L1 | Ensure Information Protection sensitivity label policies are published | Automated | Blocked | | Pending | IPPSSession requires certificate auth | + +--- + +## Section 4: Microsoft Intune + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 4.1 | L2 | Ensure devices without a compliance policy are marked 'not compliant' | Automated | Not Started | `entra.devices.device_management_settings` | Not Started | Collector exists but control logic not defined | +| 4.2 | L2 | Ensure device enrollment for personally owned devices is blocked by default | Automated | Automated | `entra.devices.enrollment_restrictions` | Implemented | | + +--- + +## Section 5: Microsoft Entra ID + +### 5.1 - User Settings + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 5.1.2.1 | L1 | Ensure 'Per-user MFA' is disabled | Automated | Manual | | N/A | Only available in beta API; not stable | +| 5.1.2.2 | L2 | Ensure third party integrated applications are not allowed | Automated | Automated | `entra.policies.authorization_policy` | Implemented | Check allowedToCreateApps | +| 5.1.2.3 | L1 | Ensure 'Restrict non-admin users from creating tenants' is set to 'Yes' | Automated | Automated | `entra.policies.authorization_policy` | Implemented | Check allowedToCreateTenants | +| 5.1.2.4 | L1 | Ensure access to the Entra admin center is restricted | Manual | Manual | | N/A | Only available via internal Azure API | +| 5.1.2.5 | L2 | Ensure the option to remain signed in is hidden | Manual | Manual | | N/A | Setting not exposed via Graph API | +| 5.1.2.6 | L2 | Ensure 'LinkedIn account connections' is disabled | Manual | Manual | | N/A | Only available via internal Azure API | +| 5.1.3.1 | L1 | Ensure a dynamic group for guest users is created | Automated | Not Started | `entra.groups.groups` | Not Started | Collector exists but control logic not defined | +| 5.1.3.2 | L1 | Ensure users cannot create security groups | Automated | Automated | `entra.policies.authorization_policy` | Implemented | Check allowedToCreateSecurityGroups | +| 5.1.4.1 | L2 | Ensure the ability to join devices to Entra is restricted | Automated | Automated | `entra.devices.device_registration_policy` | Implemented | | +| 5.1.4.2 | L1 | Ensure the maximum number of devices per user is limited | Automated | Automated | `entra.devices.device_management_settings` | Implemented | | +| 5.1.4.3 | L1 | Ensure the GA role is not added as a local administrator during Entra join | Automated | Automated | `entra.devices.device_management_settings` | Implemented | | +| 5.1.4.4 | L1 | Ensure local administrator assignment is limited during Entra join | Automated | Automated | `entra.devices.device_management_settings` | Implemented | | +| 5.1.4.5 | L1 | Ensure Local Administrator Password Solution is enabled | Automated | Not Started | | Not Started | Need LAPS configuration collector | +| 5.1.4.6 | L2 | Ensure users are restricted from recovering BitLocker keys | Automated | Automated | `entra.policies.authorization_policy` | Implemented | Check allowedToReadBitlockerKeysForOwnedDevice | +| 5.1.5.1 | L2 | Ensure user consent to apps accessing company data on their behalf is not allowed | Automated | Automated | `entra.policies.authorization_policy` | Implemented | | +| 5.1.5.2 | L1 | Ensure the admin consent workflow is enabled | Automated | Automated | `entra.policies.admin_consent_request_policy` | Implemented | | +| 5.1.6.1 | L2 | Ensure that collaboration invitations are sent to allowed domains only | Automated | Automated | `entra.policies.b2b_policy` | Implemented | | +| 5.1.6.2 | L1 | Ensure that guest user access is restricted | Automated | Automated | `entra.policies.authorization_policy` | Implemented | Check guestUserRoleId | +| 5.1.6.3 | L2 | Ensure guest user invitations are limited to the Guest Inviter role | Automated | Automated | `entra.policies.authorization_policy` | Implemented | Check allowInvitesFrom | +| 5.1.8.1 | L1 | Ensure that password hash sync is enabled for hybrid deployments | Manual | Manual | | N/A | Requires on-premises AD Connect verification | + +### 5.2 - Authentication + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 5.2.2.1 | L1 | Ensure multifactor authentication is enabled for all users in administrative roles | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires verification of all 15 admin roles | +| 5.2.2.2 | L1 | Ensure multifactor authentication is enabled for all users | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires verification of exclusions | +| 5.2.2.3 | L1 | Enable Conditional Access policies to block legacy authentication | Automated | Automated | `entra.conditional_access.legacy_auth_block` | Implemented | | +| 5.2.2.4 | L1 | Ensure Sign-in frequency is enabled and browser sessions are not persistent for Administrative users | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires admin role verification | +| 5.2.2.5 | L2 | Ensure 'Phishing-resistant MFA strength' is required for Administrators | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires admin role verification | +| 5.2.2.6 | L1 | Enable Identity Protection user risk policies | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires policy coverage verification | +| 5.2.2.7 | L1 | Enable Identity Protection sign-in risk policies | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires policy coverage verification | +| 5.2.2.8 | L2 | Ensure 'sign-in risk' is blocked for medium and high risk | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires policy coverage verification | +| 5.2.2.9 | L1 | Ensure a managed device is required for authentication | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires policy coverage verification | +| 5.2.2.10 | L1 | Ensure a managed device is required to register security information | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires policy coverage verification | +| 5.2.2.11 | L1 | Ensure sign-in frequency for Intune Enrollment is set to 'Every time' | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires policy coverage verification | +| 5.2.2.12 | L1 | Ensure the device code sign-in flow is blocked | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires policy coverage verification | +| 5.2.3.1 | L1 | Ensure Microsoft Authenticator is configured to protect against MFA fatigue | Automated | Automated | `entra.authentication.mfa_fatigue_protection` | Implemented | | +| 5.2.3.2 | L1 | Ensure custom banned passwords lists are used | Automated | Automated | `entra.authentication.password_protection` | Implemented | | +| 5.2.3.3 | L1 | Ensure password protection is enabled for on-prem Active Directory | Automated | Automated | `entra.authentication.password_protection` | Implemented | | +| 5.2.3.4 | L1 | Ensure all member users are 'MFA capable' | Automated | Automated | `entra.authentication.mfa_registration_report` | Implemented | | +| 5.2.3.5 | L1 | Ensure weak authentication methods are disabled | Automated | Automated | `entra.authentication.authentication_methods` | Implemented | Check SMS/Voice disabled | +| 5.2.3.6 | L1 | Ensure system-preferred multifactor authentication is enabled | Automated | Automated | `entra.authentication.authentication_methods` | Implemented | | +| 5.2.3.7 | L2 | Ensure the email OTP authentication method is disabled | Automated | Automated | `entra.authentication.authentication_methods` | Implemented | | +| 5.2.4.1 | L1 | Ensure 'Self service password reset enabled' is set to 'All' | Manual | Manual | | N/A | SSPR settings not exposed via Graph API | + +### 5.3 - Privileged Access + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 5.3.1 | L2 | Ensure 'Privileged Identity Management' is used to manage roles | Automated | Automated | `entra.governance.pim_role_policies` | Implemented | | +| 5.3.2 | L1 | Ensure 'Access reviews' for Guest Users are configured | Automated | Automated | `entra.governance.access_reviews` | Implemented | | +| 5.3.3 | L1 | Ensure 'Access reviews' for privileged roles are configured | Automated | Automated | `entra.governance.access_reviews` | Implemented | Combined with pim_role_policies | +| 5.3.4 | L1 | Ensure approval is required for Global Administrator role activation | Automated | Automated | `entra.governance.pim_role_policies` | Implemented | | +| 5.3.5 | L1 | Ensure approval is required for Privileged Role Administrator activation | Automated | Automated | `entra.governance.pim_role_policies` | Implemented | | + +--- + +## Section 6: Exchange Online + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 6.1.1 | L1 | Ensure 'AuditDisabled' organizationally is set to 'False' | Automated | Automated | `exchange.organization.organization_config` | Implemented | | +| 6.1.2 | L1 | Ensure mailbox audit actions are configured | Automated | Automated | `exchange.mailbox.mailbox_audit_actions` | Implemented | | +| 6.1.3 | L1 | Ensure 'AuditBypassEnabled' is not enabled on mailboxes | Automated | Automated | `exchange.mailbox.mailbox_audit` | Implemented | | +| 6.2.1 | L1 | Ensure all forms of mail forwarding are blocked and/or disabled | Automated | Automated | `exchange.transport.transport_rules` | Implemented | | +| 6.2.2 | L1 | Ensure mail transport rules do not whitelist specific domains | Automated | Automated | `exchange.transport.transport_rules` | Implemented | | +| 6.2.3 | L1 | Ensure email from external senders is identified | Automated | Automated | `exchange.transport.external_in_outlook` | Implemented | | +| 6.3.1 | L2 | Ensure users installing Outlook add-ins is not allowed | Automated | Automated | `exchange.mailbox.role_assignment_policy` | Implemented | | +| 6.5.1 | L1 | Ensure modern authentication for Exchange Online is enabled | Automated | Automated | `exchange.organization.organization_config` | Implemented | Check OAuth2ClientProfileEnabled | +| 6.5.2 | L1 | Ensure MailTips are enabled for end users | Automated | Not Started | `exchange.organization.organization_config` | Not Started | Collector exists but control logic not defined | +| 6.5.3 | L2 | Ensure additional storage providers are restricted in Outlook on the web | Automated | Automated | `exchange.organization.owa_mailbox_policy` | Implemented | | +| 6.5.4 | L1 | Ensure SMTP AUTH is disabled | Automated | Automated | `exchange.organization.organization_config` | Implemented | | +| 6.5.5 | L2 | Ensure Direct Send submissions are rejected | Automated | Not Started | `exchange.organization.transport_config` | Not Started | Collector exists but control logic not defined | + +--- + +## Section 7: SharePoint Online + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 7.2.1 | L1 | Ensure modern authentication for SharePoint applications is required | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.2.2 | L1 | Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.2.3 | L1 | Ensure external content sharing is restricted | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.2.4 | L2 | Ensure OneDrive content sharing is restricted | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.2.5 | L2 | Ensure that SharePoint guest users cannot share items they don't own | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.2.6 | L2 | Ensure SharePoint external sharing is restricted | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.2.7 | L1 | Ensure link sharing is restricted in SharePoint and OneDrive | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.2.8 | L2 | Ensure external sharing is restricted by security group | Manual | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError; CIS says Manual but can be automated | +| 7.2.9 | L1 | Ensure guest access to a site or OneDrive will expire automatically | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.2.10 | L1 | Ensure reauthentication with verification code is restricted | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.2.11 | L1 | Ensure the SharePoint default sharing link permission is set | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.3.1 | L2 | Ensure Office 365 SharePoint infected files are disallowed for download | Automated | Not Started | `sharepoint.spo_tenant` | Not Started | Collector raises NotImplementedError | +| 7.3.2 | L2 | Ensure OneDrive sync is restricted for unmanaged devices | Automated | Not Started | `sharepoint.spo_sync_client_restriction` | Not Started | Collector exists but control logic not defined | + +--- + +## Section 8: Microsoft Teams + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 8.1.1 | L2 | Ensure external file sharing in Teams is enabled for only approved cloud storage services | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.1.2 | L1 | Ensure users can't send emails to a channel email address | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.2.1 | L2 | Ensure external domains are restricted in the Teams admin center | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.2.2 | L1 | Ensure communication with unmanaged Teams users is disabled | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.2.3 | L1 | Ensure external Teams users cannot initiate conversations | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.2.4 | L1 | Ensure the organization cannot communicate with accounts in trial Teams tenants | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.4.1 | L1 | Ensure app permission policies are configured | Manual | Manual | | N/A | CIS marks as Manual; also affected by ACM migration | +| 8.5.1 | L2 | Ensure anonymous users can't join a meeting | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.5.2 | L1 | Ensure anonymous users and dial-in callers can't start a meeting | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.5.3 | L1 | Ensure only people in my org can bypass the lobby | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.5.4 | L1 | Ensure users dialing in can't bypass the lobby | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.5.5 | L2 | Ensure meeting chat does not allow anonymous users | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.5.6 | L2 | Ensure only organizers and co-organizers can present | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.5.7 | L1 | Ensure external participants can't give or request control | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.5.8 | L2 | Ensure external meeting chat is off | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.5.9 | L2 | Ensure meeting recording is off by default | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | +| 8.6.1 | L1 | Ensure users can report security concerns in Teams | Automated | Blocked | | Pending | Teams module AccessTokens auth not working | + +--- + +## Section 9: Microsoft Fabric + +| Control ID | Level | Title | CIS Suggested Audit Type | Our Audit Type | Collector ID | Status | Notes | +|------------|-------|-------|--------------------------|----------------|--------------|--------|-------| +| 9.1.1 | L1 | Ensure guest user access is restricted | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.2 | L1 | Ensure external user invitations are restricted | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.3 | L1 | Ensure guest access to content is restricted | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.4 | L1 | Ensure 'Publish to web' is restricted | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.5 | L2 | Ensure 'Interact with and share R and Python' visuals is 'Disabled' | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.6 | L1 | Ensure 'Allow users to apply sensitivity labels for content' is 'Enabled' | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.7 | L1 | Ensure shareable links are restricted | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.8 | L1 | Ensure enabling of external data sharing is restricted | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.9 | L1 | Ensure 'Block ResourceKey Authentication' is 'Enabled' | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.10 | L1 | Ensure access to APIs by service principals is restricted | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.11 | L1 | Ensure service principals cannot create and use profiles | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | +| 9.1.12 | L1 | Ensure service principals ability to create workspaces, connections and deployment pipelines is restricted | Manual | Blocked | | Pending | Fabric API auth untested; CIS says Manual but can be automated | + +--- + +## Implementation Priority + +### Priority 1: Fix Authentication Blockers + +| Blocker | Controls Affected | Resolution | +|---------|-------------------|------------| +| Teams PowerShell AccessTokens bug | 8.1.1-8.6.1 (15 controls) | Wait for Microsoft fix or implement certificate auth | +| IPPSSession certificate auth | 3.2.1, 3.2.2, 3.3.1 (3 controls) | Implement certificate-based authentication | +| Fabric API auth | 9.1.1-9.1.12 (12 controls) | Test and validate Fabric API token auth | + +### Priority 2: Implement SharePoint Collector + +| Component | Controls Affected | +|-----------|-------------------| +| `sharepoint.spo_tenant` | 7.2.1-7.2.11, 7.3.1 (12 controls) | + +### Priority 3: Complete Remaining Collectors + +Controls with collectors that exist but need control logic defined or new collectors needed. diff --git a/docs/engine/potential-folder-structure.md b/docs/engine/potential-folder-structure.md index fdc4bec4..22695b7b 100644 --- a/docs/engine/potential-folder-structure.md +++ b/docs/engine/potential-folder-structure.md @@ -111,14 +111,14 @@ Plan: Engine Folder Restructure | Current Location | New Location | |------------------------------|-----------------------------------| - | engine/engine/GCPAccess.py | engine/gcp/collector.py | - | engine/engine/aggregator.py | engine/core/evaluator.py | - | engine/engine/risk_rating.py | engine/core/risk_rating.py | - | engine/engine/json_to_pdf.py | engine/core/reporter.py | - | engine/engine/Helpers.rego | engine/shared/helpers.rego | - | engine/rules/*.rego | engine/gcp/benchmarks/cis/v3.0/ | - | engine/Rules-Azure/*.json | engine/azure/benchmarks/cis/v3.0/ | - | engine/test-configs/ | engine/gcp/test-configs/ | + | engine/legacy/engine/GCPAccess.py | engine/gcp/collector.py | + | engine/legacy/engine/aggregator.py | engine/core/evaluator.py | + | engine/legacy/engine/risk_rating.py | engine/core/risk_rating.py | + | engine/legacy/engine/json_to_pdf.py | engine/core/reporter.py | + | engine/legacy/engine/Helpers.rego | engine/shared/helpers.rego | + | engine/legacy/rules-gcp/*.rego | engine/gcp/benchmarks/cis/v3.0/ | + | engine/legacy/rules-azure/*.json | engine/azure/benchmarks/cis/v3.0/ | + | engine/legacy/test-configs/ | engine/gcp/test-configs/ | Future Extensibility diff --git a/engine/Dockerfile b/engine/Dockerfile new file mode 100644 index 00000000..d00ee5c1 --- /dev/null +++ b/engine/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies (for gevent and psycopg2) +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy and install Python dependencies +COPY pyproject.toml . +RUN pip install --no-cache-dir . + +# Copy application code +COPY worker/ ./worker/ +COPY collectors/ ./collectors/ +COPY policies/ ./policies/ +COPY opa_client.py ./ + +# Set Python path to include /app +ENV PYTHONPATH=/app + +# Copy and run entrypoint +COPY worker_entrypoint.sh . +RUN chmod +x worker_entrypoint.sh + +CMD ["./worker_entrypoint.sh"] diff --git a/engine/collectors/README.md b/engine/collectors/README.md new file mode 100644 index 00000000..704e55d6 --- /dev/null +++ b/engine/collectors/README.md @@ -0,0 +1,127 @@ +# Data Collectors + +This directory contains data collectors that fetch information from cloud APIs for compliance evaluation. Each collector gathers specific data that gets passed to OPA policies for assessment. + +## How collectors work + +A collector is a simple class with one job: call an API and return structured data. The data goes to OPA, which runs the actual compliance checks. Collectors don't make pass/fail decisions - they just gather facts. + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Collector │────▶│ Raw Data │────▶│ OPA Policy │ +│ (fetch) │ │ (dict) │ │ (evaluate) │ +└─────────────┘ └─────────────┘ └─────────────┘ +``` + +## Writing a new collector + +### 1. Create the collector class + +Inherit from `BaseDataCollector` and implement the `collect` method: + +```python +from typing import Any +from engine.collectors.base import BaseDataCollector +from engine.collectors.graph_client import GraphClient + + +class MyNewCollector(BaseDataCollector): + """One-line description of what this collects.""" + + async def collect(self, client: GraphClient) -> dict[str, Any]: + # Fetch data from the API + data = await client.get("/some/endpoint") + + # Return structured data for OPA + return { + "items": data.get("value", []), + "count": len(data.get("value", [])), + } +``` + +### 2. Place it in the right folder + +Collectors are organized by service and domain: + +``` +collectors/ + entra/ # Microsoft Entra ID (Azure AD) + roles/ # Role and admin management + conditional_access/ + m365/ # Microsoft 365 services + exchange/ # Exchange Online + sharepoint/ # SharePoint Online + teams/ # Microsoft Teams +``` + +### 3. Register it + +Why do we need to register these? + +You could dynamically import based on the string path, but that's fragile and has security implications. The registry provides: + +- Explicit allowlist - Only registered collectors can be instantiated +- Single place to see all collectors - Easy to audit what's available +- Decoupling - The scan engine doesn't need to know the import paths, just the IDs +- Validation - get_collector() raises a clear error for unknown IDs + +Add your collector to `registry.py`: + +```python +from engine.collectors.entra.roles.my_new import MyNewCollector + +DATA_COLLECTORS: dict[str, type[BaseDataCollector]] = { + # ... existing collectors ... + "entra.roles.my_new": MyNewCollector, +} +``` + +The ID follows the folder path: `entra.roles.my_new` maps to `entra/roles/my_new.py`. + +### 4. Reference it in metadata.json + +In the benchmark's `metadata.json`, set the `data_collector_id` for your control: + +```json +{ + "control_id": "CIS-1.2.3", + "data_collector_id": "entra.roles.my_new", + ... +} +``` + +## Guidelines + +**Keep collectors focused.** One collector per control, or per logical data set. If two controls need the same data, they can share a collector. + +**Return raw data.** Don't make compliance decisions in collectors. Return the facts and let OPA policies interpret them. This keeps the logic testable and the collectors reusable. + +**Handle pagination.** Use `client.get_all_pages()` for endpoints that return paged results. The Graph API often limits responses to 100 items. + +**Use type hints.** The `collect` method should return `dict[str, Any]`. Document what keys the dict contains in the docstring. + +**Async all the way.** Collectors are async. Use `await` for all API calls. This lets us run multiple collectors concurrently during scans. + +## The Graph client + +`GraphClient` handles Microsoft Graph API authentication and requests. It's shared across all Microsoft collectors (Entra, M365 services). + +```python +# Basic GET request +data = await client.get("/users") + +# GET with query parameters +data = await client.get("/users", params={"$filter": "accountEnabled eq true"}) + +# Paginated endpoint (fetches all pages) +all_users = await client.get_all_pages("/users") + +# Beta endpoint +data = await client.get("/some/beta/endpoint", beta=True) +``` + +The client handles: +- OAuth token acquisition via MSAL +- Token caching and refresh +- Pagination with @odata.nextLink +- Both v1.0 and beta Graph endpoints diff --git a/engine/collectors/__init__.py b/engine/collectors/__init__.py new file mode 100644 index 00000000..79a50f15 --- /dev/null +++ b/engine/collectors/__init__.py @@ -0,0 +1 @@ +"""Data collectors package.""" diff --git a/engine/collectors/_pending/README.md b/engine/collectors/_pending/README.md new file mode 100644 index 00000000..7eaa38ae --- /dev/null +++ b/engine/collectors/_pending/README.md @@ -0,0 +1,39 @@ +# Pending Collectors + +This directory contains collectors that have been implemented but cannot currently +be used due to authentication or API limitations. + +## Compliance Collectors + +**Issue:** Security & Compliance PowerShell (IPPSSession) does not support client +secret authentication for app-only access. + +**Reference:** https://learn.microsoft.com/en-us/powershell/exchange/connect-to-scc-powershell + +| Collector | CIS Controls | Required Cmdlet | +|-----------|--------------|-----------------| +| `dlp_compliance_policy` | 3.2.1, 3.2.2 | Get-DlpCompliancePolicy | +| `label_policy` | 3.3.1 | Get-LabelPolicy | + +**To Enable:** Implement certificate-based authentication in `PowerShellClient`, +then move these collectors back to `collectors/compliance/` and register them. + +## Teams Collectors + +**Issue:** MicrosoftTeams PowerShell module's `-AccessTokens` parameter returns +"Not supported tenant type" error for certain tenant configurations. This is a +known bug in the module. + +**Reference:** https://techcommunity.microsoft.com/discussions/teamsdeveloper/authenticating-with-an-access-token-connect-microsoftteams/2233794 + +| Collector | CIS Controls | Required Cmdlet | +|-----------|--------------|-----------------| +| `external_access_policy` | 8.2.1, 8.2.2, 8.2.3 | Get-CsExternalAccessPolicy | +| `teams_client_config` | 8.1.1, 8.1.2 | Get-CsTeamsClientConfiguration | +| `teams_meeting_policy` | 8.5.1-8.5.9 | Get-CsTeamsMeetingPolicy | +| `teams_messaging_policy` | 8.6.1 | Get-CsTeamsMessagingPolicy | +| `tenant_federation_config` | 8.2.1-8.2.4 | Get-CsTenantFederationConfiguration | + +**To Enable:** Either wait for Microsoft to fix the module, or implement +certificate-based authentication. Then move these collectors back to +`collectors/teams/` and register them. diff --git a/engine/collectors/_pending/__init__.py b/engine/collectors/_pending/__init__.py new file mode 100644 index 00000000..bbf0b24d --- /dev/null +++ b/engine/collectors/_pending/__init__.py @@ -0,0 +1,8 @@ +"""Pending collectors that are not yet ready for use. + +This directory contains collectors that have been implemented but cannot +currently be used due to authentication limitations or other issues. + +See README.md in this directory for details on each pending collector +and what is required to enable them. +""" diff --git a/engine/collectors/_pending/compliance/__init__.py b/engine/collectors/_pending/compliance/__init__.py new file mode 100644 index 00000000..df7a9bfe --- /dev/null +++ b/engine/collectors/_pending/compliance/__init__.py @@ -0,0 +1 @@ +"""Pending compliance collectors.""" diff --git a/engine/collectors/_pending/compliance/dlp_compliance_policy.py b/engine/collectors/_pending/compliance/dlp_compliance_policy.py new file mode 100644 index 00000000..70a727e8 --- /dev/null +++ b/engine/collectors/_pending/compliance/dlp_compliance_policy.py @@ -0,0 +1,64 @@ +"""DLP compliance policy collector. + +STATUS: PENDING - Requires certificate-based authentication +REASON: Security & Compliance PowerShell (IPPSSession) does not support client + secret authentication for app-only access. Only certificate-based + authentication works with Connect-IPPSSession for unattended scenarios. + See: https://learn.microsoft.com/en-us/powershell/exchange/connect-to-scc-powershell +TO ENABLE: Implement certificate-based auth in PowerShellClient, then move this + collector back to collectors/compliance/ and register it. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 3.2.1, 3.2.2 + +Connection Method: IPPSSession (Security & Compliance PowerShell via Docker container) +Authentication: Certificate-based auth required (client secret NOT supported) +Required Cmdlets: Get-DlpCompliancePolicy +Required Permissions: Exchange.ManageAsApp + Compliance role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class DlpCompliancePolicyDataCollector(BasePowerShellCollector): + """Collects DLP compliance policies for CIS compliance evaluation. + + This collector retrieves DLP policies including those covering + Teams locations for data loss prevention compliance. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect DLP compliance policy data. + + Returns: + Dict containing: + - dlp_policies: List of DLP compliance policies + - total_policies: Number of DLP policies + - teams_dlp_policies: Policies covering Teams locations + """ + policies = await client.run_cmdlet("Compliance", "Get-DlpCompliancePolicy") + + # Handle single policy vs list + if isinstance(policies, dict): + policies = [policies] + + # Find policies that cover Teams + teams_policies = [ + { + "name": p.get("Name"), + "mode": p.get("Mode"), + "enabled": p.get("Enabled"), + "teams_location": p.get("TeamsLocation"), + } + for p in policies + if p.get("TeamsLocation") + ] + + return { + "dlp_policies": policies, + "total_policies": len(policies), + "teams_dlp_policies": teams_policies, + } diff --git a/engine/collectors/_pending/compliance/label_policy.py b/engine/collectors/_pending/compliance/label_policy.py new file mode 100644 index 00000000..79b244dc --- /dev/null +++ b/engine/collectors/_pending/compliance/label_policy.py @@ -0,0 +1,63 @@ +"""Label policy collector. + +STATUS: PENDING - Requires certificate-based authentication +REASON: Security & Compliance PowerShell (IPPSSession) does not support client + secret authentication for app-only access. Only certificate-based + authentication works with Connect-IPPSSession for unattended scenarios. + See: https://learn.microsoft.com/en-us/powershell/exchange/connect-to-scc-powershell +TO ENABLE: Implement certificate-based auth in PowerShellClient, then move this + collector back to collectors/compliance/ and register it. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 3.3.1 + +Connection Method: IPPSSession (Security & Compliance PowerShell via Docker container) +Authentication: Certificate-based auth required (client secret NOT supported) +Required Cmdlets: Get-LabelPolicy +Required Permissions: Exchange.ManageAsApp + Compliance role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class LabelPolicyDataCollector(BasePowerShellCollector): + """Collects sensitivity label policies for CIS compliance evaluation. + + This collector retrieves sensitivity label publishing policies + to verify proper information protection is configured. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect label policy data. + + Returns: + Dict containing: + - label_policies: List of sensitivity label policies + - total_policies: Number of label policies + - enabled_policies: Policies that are enabled + """ + policies = await client.run_cmdlet("Compliance", "Get-LabelPolicy") + + # Handle single policy vs list + if isinstance(policies, dict): + policies = [policies] + + # Find enabled policies + enabled_policies = [ + { + "name": p.get("Name"), + "labels": p.get("Labels", []), + "exchange_location": p.get("ExchangeLocation"), + } + for p in policies + if p.get("Enabled") + ] + + return { + "label_policies": policies, + "total_policies": len(policies), + "enabled_policies": enabled_policies, + } diff --git a/engine/collectors/_pending/fabric/__init__.py b/engine/collectors/_pending/fabric/__init__.py new file mode 100644 index 00000000..81c05415 --- /dev/null +++ b/engine/collectors/_pending/fabric/__init__.py @@ -0,0 +1 @@ +"""Pending Fabric collectors - not yet ready for use.""" diff --git a/engine/collectors/_pending/fabric/tenant_settings.py b/engine/collectors/_pending/fabric/tenant_settings.py new file mode 100644 index 00000000..726027d4 --- /dev/null +++ b/engine/collectors/_pending/fabric/tenant_settings.py @@ -0,0 +1,91 @@ +"""Fabric tenant settings collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 9.1.1, 9.1.2, 9.1.3, 9.1.4, 9.1.5, 9.1.6, 9.1.7, 9.1.8, 9.1.9, 9.1.10, 9.1.11, 9.1.12 + +Control Descriptions: + 9.1.1 - Ensure guest user access is restricted + 9.1.2 - Ensure external user invitations are restricted + 9.1.3 - Ensure guest access to content is restricted + 9.1.4 - Ensure 'Publish to web' is restricted + 9.1.5 - Ensure 'Interact with and share R and Python' visuals is 'Disabled' + 9.1.6 - Ensure 'Allow users to apply sensitivity labels for content' is 'Enabled' + 9.1.7 - Ensure shareable links are restricted + 9.1.8 - Ensure enabling of external data sharing is restricted + 9.1.9 - Ensure 'Block ResourceKey Authentication' is 'Enabled' + 9.1.10 - Ensure access to APIs by service principals is restricted + 9.1.11 - Ensure service principals cannot create and use profiles + 9.1.12 - Ensure service principals ability to create workspaces is restricted + +Connection Method: Fabric Admin REST API +Required Scopes: Tenant.Read.All (Fabric API) or Fabric Administrator role +API Endpoint: GET https://api.fabric.microsoft.com/v1/admin/tenantsettings +Rate Limit: 25 requests per minute + +CAVEAT: Access token authentication for the Fabric API has not been fully tested. + It should work using MSAL token acquisition with the Fabric API scope, but this + needs verification during implementation. Certificate-based authentication may + be required instead of client secret authentication for some tenant configurations. +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class FabricTenantSettingsDataCollector(BaseDataCollector): + """Collects Microsoft Fabric tenant settings for CIS compliance evaluation. + + This collector retrieves all Fabric tenant settings via the Fabric Admin API. + Note: Requires a separate Fabric API client (not Graph API). + """ + + # Mapping of setting names to CIS controls for reference + CIS_SETTING_MAPPINGS = { + "AllowGuestUserToAccessSharedContent": "9.1.1", + "AllowExternalUserInvitations": "9.1.2", + "AllowGuestLookup": "9.1.3", + "PublishToWeb": "9.1.4", + "RScriptVisuals": "9.1.5", + "AllowSensitivityLabelToSetMIPLabel": "9.1.6", + "AllowShareableLinksForTheEntireOrganization": "9.1.7", + "AllowExternalDataSharingReceiverSwitch": "9.1.8", + "BlockResourceKeyAuthentication": "9.1.9", + "ServicePrincipalAccess": "9.1.10", + "AllowServicePrincipalsUseProfilesPreview": "9.1.11", + "CreateWorkspacesServicePrincipal": "9.1.12", + } + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect Fabric tenant settings data. + + Note: Uses FabricClient internally as Fabric API is separate from Graph. + The GraphClient is used to extract credentials for FabricClient. + + Returns: + Dict containing: + - tenant_settings: List of all tenant settings with their configurations + - settings_by_name: Dict mapping setting names to their full config + - total_settings: Number of settings returned + """ + from collectors.fabric_client import FabricClient + + # Create Fabric client using same credentials as Graph client + fabric_client = FabricClient( + tenant_id=client.tenant_id, + client_id=client.client_id, + client_secret=client.client_secret, + ) + + # Get all tenant settings + settings = await fabric_client.get_tenant_settings() + + # Build lookup by setting name for convenience + settings_by_name = {s.get("settingName"): s for s in settings} + + return { + "tenant_settings": settings, + "settings_by_name": settings_by_name, + "total_settings": len(settings), + } diff --git a/engine/collectors/_pending/teams/__init__.py b/engine/collectors/_pending/teams/__init__.py new file mode 100644 index 00000000..47fdfa2e --- /dev/null +++ b/engine/collectors/_pending/teams/__init__.py @@ -0,0 +1 @@ +"""Pending Teams collectors.""" diff --git a/engine/collectors/_pending/teams/external_access_policy.py b/engine/collectors/_pending/teams/external_access_policy.py new file mode 100644 index 00000000..2fa9d0b3 --- /dev/null +++ b/engine/collectors/_pending/teams/external_access_policy.py @@ -0,0 +1,58 @@ +"""External access policy collector. + +STATUS: PENDING - MicrosoftTeams module AccessTokens auth not working +REASON: Connect-MicrosoftTeams with -AccessTokens parameter returns + "Not supported tenant type" error. This is a known bug in the + MicrosoftTeams PowerShell module affecting certain tenant configurations. + See: https://techcommunity.microsoft.com/discussions/teamsdeveloper/authenticating-with-an-access-token-connect-microsoftteams/2233794 +TO ENABLE: Either wait for Microsoft to fix the module, or implement + certificate-based auth. Then move this collector back to + collectors/teams/ and register it. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 8.2.1, 8.2.2, 8.2.3 + +Connection Method: Microsoft Teams PowerShell (via Docker container) +Authentication: AccessTokens auth currently broken - certificate auth may work +Required Cmdlets: Get-CsExternalAccessPolicy +Required Permissions: Teams.ManageAsApp + Teams Admin role +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class ExternalAccessPolicyDataCollector(BasePowerShellCollector): + """Collects external access policy for CIS compliance evaluation. + + This collector retrieves external access settings for Teams + communication with external organizations. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect external access policy data. + + Returns: + Dict containing: + - external_access_policies: List of external access policies + - global_policy: The global external access policy + """ + policies = await client.run_cmdlet("Teams", "Get-CsExternalAccessPolicy") + + # Handle single policy vs list + if isinstance(policies, dict): + policies = [policies] + + # Find global policy + global_policy = next( + (p for p in policies if p.get("Identity") == "Global"), + None + ) + + return { + "external_access_policies": policies, + "total_policies": len(policies), + "global_policy": global_policy, + } diff --git a/engine/collectors/_pending/teams/teams_client_config.py b/engine/collectors/_pending/teams/teams_client_config.py new file mode 100644 index 00000000..497ab63f --- /dev/null +++ b/engine/collectors/_pending/teams/teams_client_config.py @@ -0,0 +1,55 @@ +"""Teams client configuration collector. + +STATUS: PENDING - MicrosoftTeams module AccessTokens auth not working +REASON: Connect-MicrosoftTeams with -AccessTokens parameter returns + "Not supported tenant type" error. This is a known bug in the + MicrosoftTeams PowerShell module affecting certain tenant configurations. + See: https://techcommunity.microsoft.com/discussions/teamsdeveloper/authenticating-with-an-access-token-connect-microsoftteams/2233794 +TO ENABLE: Either wait for Microsoft to fix the module, or implement + certificate-based auth. Then move this collector back to + collectors/teams/ and register it. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 8.1.1, 8.1.2 + +Connection Method: Microsoft Teams PowerShell (via Docker container) +Authentication: AccessTokens auth currently broken - certificate auth may work +Required Cmdlets: Get-CsTeamsClientConfiguration +Required Permissions: Teams.ManageAsApp + Teams Admin role +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class TeamsClientConfigDataCollector(BasePowerShellCollector): + """Collects Teams client configuration for CIS compliance evaluation. + + This collector retrieves Teams client settings including external + storage providers and email to channel configurations. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect Teams client configuration data. + + Returns: + Dict containing: + - teams_client_config: Full client configuration + - allow_dropbox: Dropbox integration status + - allow_box: Box integration status + - allow_google_drive: Google Drive integration status + - allow_sharefile: ShareFile integration status + - allow_email_into_channel: Email to channel status + """ + config = await client.run_cmdlet("Teams", "Get-CsTeamsClientConfiguration") + + return { + "teams_client_config": config, + "allow_dropbox": config.get("AllowDropBox") if config else None, + "allow_box": config.get("AllowBox") if config else None, + "allow_google_drive": config.get("AllowGoogleDrive") if config else None, + "allow_sharefile": config.get("AllowShareFile") if config else None, + "allow_email_into_channel": config.get("AllowEmailIntoChannel") if config else None, + } diff --git a/engine/collectors/_pending/teams/teams_meeting_policy.py b/engine/collectors/_pending/teams/teams_meeting_policy.py new file mode 100644 index 00000000..76e715f0 --- /dev/null +++ b/engine/collectors/_pending/teams/teams_meeting_policy.py @@ -0,0 +1,67 @@ +"""Teams meeting policy collector. + +STATUS: PENDING - MicrosoftTeams module AccessTokens auth not working +REASON: Connect-MicrosoftTeams with -AccessTokens parameter returns + "Not supported tenant type" error. This is a known bug in the + MicrosoftTeams PowerShell module affecting certain tenant configurations. + See: https://techcommunity.microsoft.com/discussions/teamsdeveloper/authenticating-with-an-access-token-connect-microsoftteams/2233794 +TO ENABLE: Either wait for Microsoft to fix the module, or implement + certificate-based auth. Then move this collector back to + collectors/teams/ and register it. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 8.5.1, 8.5.2, 8.5.3, 8.5.4, 8.5.5, 8.5.6, 8.5.7, 8.5.8, 8.5.9 + +Connection Method: Microsoft Teams PowerShell (via Docker container) +Authentication: AccessTokens auth currently broken - certificate auth may work +Required Cmdlets: Get-CsTeamsMeetingPolicy +Required Permissions: Teams.ManageAsApp + Teams Admin role +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class TeamsMeetingPolicyDataCollector(BasePowerShellCollector): + """Collects Teams meeting policies for CIS compliance evaluation. + + This collector retrieves meeting settings including lobby, anonymous + users, recording, and presenter controls. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect Teams meeting policy data. + + Returns: + Dict containing: + - meeting_policies: List of meeting policies + - global_policy: The global meeting policy + """ + policies = await client.run_cmdlet("Teams", "Get-CsTeamsMeetingPolicy") + + # Handle single policy vs list + if isinstance(policies, dict): + policies = [policies] + + # Find global policy + global_policy = next( + (p for p in policies if p.get("Identity") == "Global"), + None + ) + + return { + "meeting_policies": policies, + "total_policies": len(policies), + "global_policy": global_policy, + # Key settings for CIS controls + "allow_anonymous_users_to_join": global_policy.get("AllowAnonymousUsersToJoinMeeting") if global_policy else None, + "allow_anonymous_users_to_start": global_policy.get("AllowAnonymousUsersToStartMeeting") if global_policy else None, + "auto_admitted_users": global_policy.get("AutoAdmittedUsers") if global_policy else None, + "allow_pstn_users_to_bypass_lobby": global_policy.get("AllowPSTNUsersToBypassLobby") if global_policy else None, + "meeting_chat_enabled_type": global_policy.get("MeetingChatEnabledType") if global_policy else None, + "designated_presenter_role_mode": global_policy.get("DesignatedPresenterRoleMode") if global_policy else None, + "allow_cloud_recording": global_policy.get("AllowCloudRecording") if global_policy else None, + "allow_external_participant_give_request_control": global_policy.get("AllowExternalParticipantGiveRequestControl") if global_policy else None, + } diff --git a/engine/collectors/_pending/teams/teams_messaging_policy.py b/engine/collectors/_pending/teams/teams_messaging_policy.py new file mode 100644 index 00000000..51c34bf3 --- /dev/null +++ b/engine/collectors/_pending/teams/teams_messaging_policy.py @@ -0,0 +1,60 @@ +"""Teams messaging policy collector. + +STATUS: PENDING - MicrosoftTeams module AccessTokens auth not working +REASON: Connect-MicrosoftTeams with -AccessTokens parameter returns + "Not supported tenant type" error. This is a known bug in the + MicrosoftTeams PowerShell module affecting certain tenant configurations. + See: https://techcommunity.microsoft.com/discussions/teamsdeveloper/authenticating-with-an-access-token-connect-microsoftteams/2233794 +TO ENABLE: Either wait for Microsoft to fix the module, or implement + certificate-based auth. Then move this collector back to + collectors/teams/ and register it. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 8.6.1 + +Connection Method: Microsoft Teams PowerShell (via Docker container) +Authentication: AccessTokens auth currently broken - certificate auth may work +Required Cmdlets: Get-CsTeamsMessagingPolicy +Required Permissions: Teams.ManageAsApp + Teams Admin role +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class TeamsMessagingPolicyDataCollector(BasePowerShellCollector): + """Collects Teams messaging policies for CIS compliance evaluation. + + This collector retrieves messaging settings including user reporting + for security concerns. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect Teams messaging policy data. + + Returns: + Dict containing: + - messaging_policies: List of messaging policies + - global_policy: The global messaging policy + - allow_security_end_user_reporting: User security reporting status + """ + policies = await client.run_cmdlet("Teams", "Get-CsTeamsMessagingPolicy") + + # Handle single policy vs list + if isinstance(policies, dict): + policies = [policies] + + # Find global policy + global_policy = next( + (p for p in policies if p.get("Identity") == "Global"), + None + ) + + return { + "messaging_policies": policies, + "total_policies": len(policies), + "global_policy": global_policy, + "allow_security_end_user_reporting": global_policy.get("AllowSecurityEndUserReporting") if global_policy else None, + } diff --git a/engine/collectors/_pending/teams/tenant_federation_config.py b/engine/collectors/_pending/teams/tenant_federation_config.py new file mode 100644 index 00000000..4fba3778 --- /dev/null +++ b/engine/collectors/_pending/teams/tenant_federation_config.py @@ -0,0 +1,56 @@ +"""Tenant federation configuration collector. + +STATUS: PENDING - MicrosoftTeams module AccessTokens auth not working +REASON: Connect-MicrosoftTeams with -AccessTokens parameter returns + "Not supported tenant type" error. This is a known bug in the + MicrosoftTeams PowerShell module affecting certain tenant configurations. + See: https://techcommunity.microsoft.com/discussions/teamsdeveloper/authenticating-with-an-access-token-connect-microsoftteams/2233794 +TO ENABLE: Either wait for Microsoft to fix the module, or implement + certificate-based auth. Then move this collector back to + collectors/teams/ and register it. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 8.2.1, 8.2.2, 8.2.3, 8.2.4 + +Connection Method: Microsoft Teams PowerShell (via Docker container) +Authentication: AccessTokens auth currently broken - certificate auth may work +Required Cmdlets: Get-CsTenantFederationConfiguration +Required Permissions: Teams.ManageAsApp + Teams Admin role +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class TenantFederationConfigDataCollector(BasePowerShellCollector): + """Collects tenant federation configuration for CIS compliance evaluation. + + This collector retrieves federation settings including unmanaged users + and trial tenant communication settings. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect tenant federation configuration data. + + Returns: + Dict containing: + - federation_config: Full federation configuration + - allow_federated_users: Federated user access status + - allow_public_users: Public (Skype) user access status + - allow_teams_consumer: Teams consumer access status + - allowed_domains: Allowed federation domains + - blocked_domains: Blocked federation domains + """ + config = await client.run_cmdlet("Teams", "Get-CsTenantFederationConfiguration") + + return { + "federation_config": config, + "allow_federated_users": config.get("AllowFederatedUsers") if config else None, + "allow_public_users": config.get("AllowPublicUsers") if config else None, + "allow_teams_consumer": config.get("AllowTeamsConsumer") if config else None, + "allow_teams_consumer_inbound": config.get("AllowTeamsConsumerInbound") if config else None, + "allowed_domains": config.get("AllowedDomains") if config else None, + "blocked_domains": config.get("BlockedDomains") if config else None, + } diff --git a/engine/collectors/base.py b/engine/collectors/base.py new file mode 100644 index 00000000..e933e232 --- /dev/null +++ b/engine/collectors/base.py @@ -0,0 +1,23 @@ +"""Base class for data collectors.""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collectors.graph_client import GraphClient + + +class BaseDataCollector(ABC): + """Abstract base class for data collectors.""" + + @abstractmethod + async def collect(self, client: "GraphClient") -> dict[str, Any]: + """Collect data from the cloud API. + + Args: + client: The API client to use for data collection. + + Returns: + Dictionary of collected data to be passed to OPA for evaluation. + """ + pass diff --git a/engine/collectors/compliance/__init__.py b/engine/collectors/compliance/__init__.py new file mode 100644 index 00000000..af255464 --- /dev/null +++ b/engine/collectors/compliance/__init__.py @@ -0,0 +1,6 @@ +"""Security & Compliance collectors. + +These collectors use the Security & Compliance PowerShell via IPPSSession. +Authentication uses client secret via MSAL to obtain access tokens, +which are passed to PowerShell cmdlets via the -AccessToken parameter. +""" diff --git a/engine/collectors/compliance/report_submission_policy.py b/engine/collectors/compliance/report_submission_policy.py new file mode 100644 index 00000000..2df01a82 --- /dev/null +++ b/engine/collectors/compliance/report_submission_policy.py @@ -0,0 +1,46 @@ +"""Report submission policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 8.6.1 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-ReportSubmissionPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment + +Note: Despite being related to security reporting, this cmdlet is in ExchangeOnline, +not in the Compliance module. +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class ReportSubmissionPolicyDataCollector(BasePowerShellCollector): + """Collects report submission policy for CIS compliance evaluation. + + This collector retrieves user reporting settings for Microsoft Defender + to verify security concern reporting is properly configured. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect report submission policy data. + + Returns: + Dict containing: + - report_submission_policy: The report submission policy + - report_junk_to_customized_address: Custom junk reporting address + - report_not_junk_to_customized_address: Custom not-junk reporting address + - report_phish_to_customized_address: Custom phish reporting address + """ + policy = await client.run_cmdlet("ExchangeOnline", "Get-ReportSubmissionPolicy") + + return { + "report_submission_policy": policy, + "report_junk_to_customized_address": policy.get("ReportJunkToCustomizedAddress") if policy else None, + "report_not_junk_to_customized_address": policy.get("ReportNotJunkToCustomizedAddress") if policy else None, + "report_phish_to_customized_address": policy.get("ReportPhishToCustomizedAddress") if policy else None, + "enable_reported_message_to_microsoft": policy.get("EnableReportToMicrosoft") if policy else None, + } diff --git a/engine/collectors/entra/__init__.py b/engine/collectors/entra/__init__.py new file mode 100644 index 00000000..3c58d97e --- /dev/null +++ b/engine/collectors/entra/__init__.py @@ -0,0 +1 @@ +"""Microsoft Entra ID collectors.""" diff --git a/engine/collectors/entra/applications/__init__.py b/engine/collectors/entra/applications/__init__.py new file mode 100644 index 00000000..229753ee --- /dev/null +++ b/engine/collectors/entra/applications/__init__.py @@ -0,0 +1 @@ +"""Entra ID Applications collectors.""" diff --git a/engine/collectors/entra/applications/apps_and_services_settings.py b/engine/collectors/entra/applications/apps_and_services_settings.py new file mode 100644 index 00000000..43b00837 --- /dev/null +++ b/engine/collectors/entra/applications/apps_and_services_settings.py @@ -0,0 +1,44 @@ +"""Apps and services settings collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.3.4 + +Connection Method: Microsoft Graph API +Required Scopes: OrgSettings-AppsAndServices.Read.All +Graph Endpoint: /admin/serviceAnnouncement/messages (or organization settings) +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class AppsAndServicesSettingsDataCollector(BaseDataCollector): + """Collects apps and services settings for CIS compliance evaluation. + + This collector retrieves user-owned apps and services settings + to verify proper application governance configuration. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect apps and services settings data. + + Returns: + Dict containing: + - apps_and_services_settings: Apps and services configuration + - user_owned_apps_enabled: Whether users can own apps + """ + # Get organization settings for apps and services + # This uses the admin/microsoft365Apps settings endpoint + try: + settings = await client.get("/admin/microsoft365Apps/settings", beta=True) + except Exception: + # Fallback if endpoint not available + settings = {} + + return { + "apps_and_services_settings": settings, + "user_owned_apps_enabled": settings.get("isUserAppsAndServicesEnabled"), + "is_office_store_enabled": settings.get("isOfficeStoreEnabled"), + } diff --git a/engine/collectors/entra/applications/forms_settings.py b/engine/collectors/entra/applications/forms_settings.py new file mode 100644 index 00000000..56452289 --- /dev/null +++ b/engine/collectors/entra/applications/forms_settings.py @@ -0,0 +1,59 @@ +"""Forms settings collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.3.5 + +Connection Method: Microsoft Graph API +Required Scopes: OrgSettings-Forms.Read.All +Graph Endpoint: /admin/forms/settings +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class FormsSettingsDataCollector(BaseDataCollector): + """Collects Microsoft Forms settings for CIS compliance evaluation. + + This collector retrieves Microsoft Forms configuration including + phishing protection settings for compliance evaluation. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect Forms settings data. + + Returns: + Dict containing: + - forms_settings: Microsoft Forms configuration + - internal_phishing_protection_enabled: Phishing protection status + - external_sharing_enabled: External sharing status + """ + # Get Microsoft Forms admin settings + try: + settings = await client.get("/admin/forms/settings", beta=True) + except Exception: + settings = {} + + return { + "forms_settings": settings, + "internal_phishing_protection_enabled": settings.get( + "isInternalPhishingProtectionEnabled" + ), + "external_sharing_enabled": settings.get("isExternalSharingEnabled"), + "external_send_form_enabled": settings.get("isExternalSendFormEnabled"), + "external_share_collaborating_enabled": settings.get( + "isExternalShareCollaborationEnabled" + ), + "external_share_template_enabled": settings.get( + "isExternalShareTemplateEnabled" + ), + "external_share_result_enabled": settings.get( + "isExternalShareResultEnabled" + ), + "bing_search_enabled": settings.get("isBingSearchEnabled"), + "record_identity_by_default_enabled": settings.get( + "isRecordIdentityByDefaultEnabled" + ), + } diff --git a/engine/collectors/entra/applications/service_principals.py b/engine/collectors/entra/applications/service_principals.py new file mode 100644 index 00000000..66e39afd --- /dev/null +++ b/engine/collectors/entra/applications/service_principals.py @@ -0,0 +1,61 @@ +"""Service principals collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.3.7 + +Connection Method: Microsoft Graph API +Required Scopes: Application.Read.All +Graph Endpoint: /servicePrincipals +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class ServicePrincipalsDataCollector(BaseDataCollector): + """Collects service principal information for CIS compliance evaluation. + + This collector retrieves service principal details to verify + third-party storage service principal status and app configurations. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect service principals data. + + Returns: + Dict containing: + - service_principals: List of service principals + - total_service_principals: Number of service principals + - third_party_storage_apps: Third-party storage service principals + """ + # Get all service principals + service_principals = await client.get_all_pages("/servicePrincipals") + + # Known third-party storage app IDs (Dropbox, Google Drive, Box, etc.) + third_party_storage_app_names = [ + "dropbox", + "google drive", + "box", + "egnyte", + "citrix sharefile", + ] + + # Filter for third-party storage apps + third_party_storage_apps = [ + sp + for sp in service_principals + if any( + name in (sp.get("displayName") or "").lower() + for name in third_party_storage_app_names + ) + ] + + return { + "service_principals": service_principals, + "total_service_principals": len(service_principals), + "third_party_storage_apps": third_party_storage_apps, + "third_party_storage_apps_count": len(third_party_storage_apps), + "has_third_party_storage_apps": len(third_party_storage_apps) > 0, + } diff --git a/engine/collectors/entra/authentication/__init__.py b/engine/collectors/entra/authentication/__init__.py new file mode 100644 index 00000000..5093500d --- /dev/null +++ b/engine/collectors/entra/authentication/__init__.py @@ -0,0 +1 @@ +"""Entra ID Authentication collectors.""" diff --git a/engine/collectors/entra/authentication/authentication_methods.py b/engine/collectors/entra/authentication/authentication_methods.py new file mode 100644 index 00000000..4906fc9f --- /dev/null +++ b/engine/collectors/entra/authentication/authentication_methods.py @@ -0,0 +1,66 @@ +"""Authentication methods collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.2.3.1, 5.2.3.5, 5.2.3.6, 5.2.3.7 + +Connection Method: Microsoft Graph API +Required Scopes: Policy.Read.All, Policy.Read.AuthenticationMethod +Graph Endpoint: /policies/authenticationMethodsPolicy +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class AuthenticationMethodsDataCollector(BaseDataCollector): + """Collects authentication method configurations for CIS compliance evaluation. + + This collector retrieves authentication method policies including + SMS, Voice, Email OTP, and other method configurations. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect authentication methods policy data. + + Returns: + Dict containing: + - authentication_methods_policy: The authentication methods policy + - method_configurations: Individual method configurations + - sms_enabled: Whether SMS authentication is enabled + - voice_enabled: Whether voice call authentication is enabled + - email_otp_enabled: Whether email OTP is enabled + """ + # Get the authentication methods policy + policy = await client.get("/policies/authenticationMethodsPolicy", beta=True) + + # Get individual method configurations + method_configs = policy.get("authenticationMethodConfigurations", []) + + # Build lookup by method type + methods_by_type = {} + for config in method_configs: + method_id = config.get("id", "") + methods_by_type[method_id] = config + + # Check specific method states + def is_method_enabled(method_id: str) -> bool | None: + method = methods_by_type.get(method_id) + if method is None: + return None + return method.get("state") == "enabled" + + return { + "authentication_methods_policy": policy, + "method_configurations": method_configs, + "methods_by_type": methods_by_type, + "sms_enabled": is_method_enabled("Sms"), + "voice_enabled": is_method_enabled("Voice"), + "email_otp_enabled": is_method_enabled("Email"), + "fido2_enabled": is_method_enabled("Fido2"), + "microsoft_authenticator_enabled": is_method_enabled("MicrosoftAuthenticator"), + "temporary_access_pass_enabled": is_method_enabled("TemporaryAccessPass"), + "software_oath_enabled": is_method_enabled("SoftwareOath"), + "hardware_oath_enabled": is_method_enabled("HardwareOath"), + } diff --git a/engine/collectors/entra/authentication/mfa_fatigue_protection.py b/engine/collectors/entra/authentication/mfa_fatigue_protection.py new file mode 100644 index 00000000..5decf7f2 --- /dev/null +++ b/engine/collectors/entra/authentication/mfa_fatigue_protection.py @@ -0,0 +1,78 @@ +"""MFA fatigue protection collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.2.3.1 + +Connection Method: Microsoft Graph API +Required Scopes: Policy.Read.All +Graph Endpoint: /policies/authenticationMethodsPolicy/authenticationMethodConfigurations/MicrosoftAuthenticator + +Control covered: + - 5.2.3.1: Ensure Microsoft Authenticator is configured to protect against MFA fatigue +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class MfaFatigueProtectionDataCollector(BaseDataCollector): + """Collects Microsoft Authenticator MFA fatigue protection settings. + + This collector retrieves Microsoft Authenticator configuration to verify + that number matching and additional context are enabled to protect + against MFA fatigue attacks. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect Microsoft Authenticator MFA fatigue protection settings. + + Returns: + Dict containing: + - authenticator_config: Full Microsoft Authenticator configuration + - state: Whether Microsoft Authenticator is enabled/disabled + - number_matching_enabled: Whether number matching is enabled + - display_app_information_enabled: Whether app context is shown + - display_location_information_enabled: Whether location context is shown + - feature_settings: Detailed feature settings configuration + - mfa_fatigue_protection_enabled: Overall assessment of MFA fatigue protection + """ + # Get Microsoft Authenticator configuration + config = await client.get( + "/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/MicrosoftAuthenticator", + beta=True, + ) + + # Extract feature settings + feature_settings = config.get("featureSettings", {}) + + # Extract individual feature states + number_matching_state = feature_settings.get("numberMatchingRequiredState", {}) + display_app_state = feature_settings.get("displayAppInformationRequiredState", {}) + display_location_state = feature_settings.get("displayLocationInformationRequiredState", {}) + + # Determine if features are enabled + # State can be "enabled", "disabled", or "default" + number_matching_enabled = number_matching_state.get("state") == "enabled" + display_app_enabled = display_app_state.get("state") == "enabled" + display_location_enabled = display_location_state.get("state") == "enabled" + + # MFA fatigue protection is considered enabled if number matching is enabled + # Additional context (app info, location) provides extra protection + mfa_fatigue_protection_enabled = number_matching_enabled + + return { + "authenticator_config": config, + "state": config.get("state"), + "number_matching_enabled": number_matching_enabled, + "number_matching_state": number_matching_state, + "display_app_information_enabled": display_app_enabled, + "display_app_information_state": display_app_state, + "display_location_information_enabled": display_location_enabled, + "display_location_information_state": display_location_state, + "feature_settings": feature_settings, + "mfa_fatigue_protection_enabled": mfa_fatigue_protection_enabled, + "include_targets": config.get("includeTargets", []), + "exclude_targets": config.get("excludeTargets", []), + } diff --git a/engine/collectors/entra/authentication/mfa_registration_report.py b/engine/collectors/entra/authentication/mfa_registration_report.py new file mode 100644 index 00000000..36a630ea --- /dev/null +++ b/engine/collectors/entra/authentication/mfa_registration_report.py @@ -0,0 +1,65 @@ +"""MFA registration report collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.2.3.4 + +Connection Method: Microsoft Graph API +Required Scopes: UserAuthenticationMethod.Read.All, AuditLog.Read.All +Graph Endpoint: /reports/authenticationMethods/userRegistrationDetails +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class MFARegistrationReportDataCollector(BaseDataCollector): + """Collects MFA registration report for CIS compliance evaluation. + + This collector retrieves MFA capable user registration details + to verify users have registered for multi-factor authentication. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect MFA registration report data. + + Returns: + Dict containing: + - registration_details: User MFA registration details + - total_users: Total number of users + - mfa_registered_count: Number of users registered for MFA + - mfa_capable_count: Number of users capable of MFA + """ + # Get user registration details for MFA + registration_details = await client.get_all_pages( + "/reports/authenticationMethods/userRegistrationDetails", + beta=True, + ) + + # Count users by MFA status + mfa_registered_count = 0 + mfa_capable_count = 0 + + for user in registration_details: + # isMfaRegistered indicates user has registered for MFA + if user.get("isMfaRegistered"): + mfa_registered_count += 1 + # isMfaCapable indicates user can use MFA + if user.get("isMfaCapable"): + mfa_capable_count += 1 + + total_users = len(registration_details) + + return { + "registration_details": registration_details, + "total_users": total_users, + "mfa_registered_count": mfa_registered_count, + "mfa_capable_count": mfa_capable_count, + "mfa_not_registered_count": total_users - mfa_registered_count, + "mfa_registration_percentage": ( + round(mfa_registered_count / total_users * 100, 2) + if total_users > 0 + else 0 + ), + } diff --git a/engine/collectors/entra/authentication/password_protection.py b/engine/collectors/entra/authentication/password_protection.py new file mode 100644 index 00000000..7ece0bcc --- /dev/null +++ b/engine/collectors/entra/authentication/password_protection.py @@ -0,0 +1,61 @@ +"""Password protection collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.2.3.2, 5.2.3.3 + +Connection Method: Microsoft Graph API +Required Scopes: Directory.Read.All +Graph Endpoint: /settings (directory settings) +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class PasswordProtectionDataCollector(BaseDataCollector): + """Collects password protection settings for CIS compliance evaluation. + + This collector retrieves custom banned password list configuration + and on-premises password protection settings. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect password protection data. + + Returns: + Dict containing: + - password_protection_settings: Password protection configuration + - banned_password_list_enabled: Whether custom banned list is enabled + - on_prem_protection_enabled: Whether on-prem protection is enabled + - lockout_settings: Account lockout configuration + """ + # Get directory settings which include password rule settings + settings_response = await client.get("/settings", beta=True) + settings_list = settings_response.get("value", []) + + # Find the password rule settings template + password_settings = None + for setting in settings_list: + if setting.get("templateId") == "5cf42378-d67d-4f36-ba46-e8b86229381d": + # Password Rule Settings + password_settings = setting + break + + # Extract values from settings + values = {} + if password_settings: + for value in password_settings.get("values", []): + values[value.get("name")] = value.get("value") + + return { + "password_protection_settings": password_settings, + "settings_values": values, + "banned_password_list_enabled": values.get("EnableBannedPasswordCheck") == "True", + "banned_password_list": values.get("BannedPasswordList"), + "on_prem_protection_enabled": values.get("EnableBannedPasswordCheckOnPremises") == "True", + "lockout_threshold": values.get("LockoutThreshold"), + "lockout_duration_in_seconds": values.get("LockoutDurationInSeconds"), + "enforce_custom_banned_passwords": values.get("BannedPasswordCheckOnPremisesMode"), + } diff --git a/engine/collectors/entra/conditional_access/__init__.py b/engine/collectors/entra/conditional_access/__init__.py new file mode 100644 index 00000000..5f7bf092 --- /dev/null +++ b/engine/collectors/entra/conditional_access/__init__.py @@ -0,0 +1 @@ +"""Entra ID Conditional Access collectors.""" diff --git a/engine/collectors/entra/conditional_access/conditional_access_policies.py b/engine/collectors/entra/conditional_access/conditional_access_policies.py new file mode 100644 index 00000000..cf26225a --- /dev/null +++ b/engine/collectors/entra/conditional_access/conditional_access_policies.py @@ -0,0 +1,132 @@ +"""Conditional Access policies collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.3.2, 5.2.2.1, 5.2.2.2, 5.2.2.3, 5.2.2.4, 5.2.2.5, 5.2.2.6, + 5.2.2.7, 5.2.2.8, 5.2.2.9, 5.2.2.10, 5.2.2.11, 5.2.2.12 + +Connection Method: Microsoft Graph API +Required Scopes: Policy.Read.All +Graph Endpoint: /identity/conditionalAccess/policies + +Automation Status: + FULLY AUTOMATABLE: + - 5.2.2.3: Block legacy authentication (check for policy blocking legacy client types) + + REQUIRES USER INVOLVEMENT (not yet automatable): + - 5.2.2.1: MFA for admins - requires verifying ALL admin roles are covered + - 5.2.2.2: MFA for all users - requires verifying exclusions are legitimate + - 5.2.2.4: Sign-in frequency for admins - requires admin role verification + - 5.2.2.5: Phishing-resistant MFA for admins - requires admin role verification + - 5.2.2.6: User risk policy - requires verifying policy coverage + - 5.2.2.7: Sign-in risk policy - requires verifying policy coverage + - 5.2.2.8: Sign-in risk blocked - requires verifying policy coverage + - 5.2.2.9: Managed device required - requires verifying policy coverage + - 5.2.2.10: Managed device for security info - requires verifying policy coverage + - 5.2.2.11: Intune enrollment sign-in frequency - requires verifying policy coverage + - 5.2.2.12: Device code flow blocked - requires verifying policy coverage + - 1.3.2: Sign-in session policies - requires verifying policy coverage + + These controls require "compliant with review" status where exclusions and + policy gaps are reported for manual verification. This capability is not + yet implemented in the evaluation framework. +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class ConditionalAccessPoliciesDataCollector(BaseDataCollector): + """Collects Conditional Access policies for CIS compliance evaluation. + + This collector retrieves all Conditional Access policies with full + configuration details for evaluating MFA, device compliance, session + controls, and other security requirements. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect Conditional Access policy data. + + Returns: + Dict containing: + - policies: List of CA policies with full configuration + - total_policies: Number of policies + - enabled_policies: List of enabled policies + - enabled_policies_count: Number of enabled policies + - report_only_policies_count: Number of report-only policies + - disabled_policies_count: Number of disabled policies + - policies_requiring_mfa: List of policies requiring MFA + - policies_blocking_legacy_auth: List of policies blocking legacy auth + - policies_requiring_compliant_device: List of policies requiring device compliance + + Note: OPA can use count() to determine array lengths, so *_count + properties are only provided for top-level policy state counts. + """ + # Get all Conditional Access policies + policies = await client.get_conditional_access_policies() + + # Categorize policies by state + enabled_policies = [p for p in policies if p.get("state") == "enabled"] + report_only_policies = [ + p for p in policies + if p.get("state") == "enabledForReportingButNotEnforced" + ] + disabled_policies = [p for p in policies if p.get("state") == "disabled"] + + # Identify policies by function + policies_requiring_mfa = [] + policies_blocking_legacy_auth = [] + policies_requiring_compliant_device = [] + + for policy in enabled_policies: + grant_controls = policy.get("grantControls") or {} + built_in_controls = grant_controls.get("builtInControls", []) + conditions = policy.get("conditions") or {} + client_app_types = conditions.get("clientAppTypes", []) + + # Check for MFA requirement + if "mfa" in built_in_controls: + policies_requiring_mfa.append({ + "id": policy.get("id"), + "displayName": policy.get("displayName"), + "state": policy.get("state"), + "conditions": conditions, + "grantControls": grant_controls, + }) + + # Check for legacy auth blocking + # Legacy auth is blocked when clientAppTypes includes legacy types and action is block + legacy_types = {"exchangeActiveSync", "other"} + if legacy_types.intersection(set(client_app_types)): + if "block" in built_in_controls: + policies_blocking_legacy_auth.append({ + "id": policy.get("id"), + "displayName": policy.get("displayName"), + "state": policy.get("state"), + "conditions": conditions, + "grantControls": grant_controls, + }) + + # Check for compliant device requirement + if "compliantDevice" in built_in_controls: + policies_requiring_compliant_device.append({ + "id": policy.get("id"), + "displayName": policy.get("displayName"), + "state": policy.get("state"), + "conditions": conditions, + "grantControls": grant_controls, + }) + + return { + "policies": policies, + "total_policies": len(policies), + "enabled_policies": enabled_policies, + "enabled_policies_count": len(enabled_policies), + "report_only_policies_count": len(report_only_policies), + "disabled_policies_count": len(disabled_policies), + "policies_requiring_mfa": policies_requiring_mfa, + "policies_requiring_mfa_count": len(policies_requiring_mfa), + "policies_blocking_legacy_auth": policies_blocking_legacy_auth, + "policies_requiring_compliant_device": policies_requiring_compliant_device, + } diff --git a/engine/collectors/entra/conditional_access/legacy_auth_block.py b/engine/collectors/entra/conditional_access/legacy_auth_block.py new file mode 100644 index 00000000..dc7469df --- /dev/null +++ b/engine/collectors/entra/conditional_access/legacy_auth_block.py @@ -0,0 +1,67 @@ +"""Legacy authentication block collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.2.2.3 + +Connection Method: Microsoft Graph API +Required Scopes: Policy.Read.All +Graph Endpoint: /identity/conditionalAccess/policies +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class LegacyAuthBlockDataCollector(BaseDataCollector): + """Collects Conditional Access policies that block legacy authentication.""" + + LEGACY_CLIENT_TYPES = {"exchangeActiveSync", "other"} + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect Conditional Access policies and check for legacy auth blocking. + + Returns: + Dict with conditional_access_policies list. + """ + policies = await client.get_conditional_access_policies() + + policy_data = [] + for policy in policies: + conditions = policy.get("conditions", {}) + users = conditions.get("users", {}) + apps = conditions.get("applications", {}) + client_app_types = set(conditions.get("clientAppTypes", [])) + grant_controls = policy.get("grantControls", {}) or {} + + # Check if policy targets all users + include_users = users.get("includeUsers", []) + targets_all_users = "All" in include_users + + # Check if policy targets all apps + include_apps = apps.get("includeApplications", []) + targets_all_apps = "All" in include_apps + + # Check if policy blocks legacy auth client types + blocks_legacy = self.LEGACY_CLIENT_TYPES.issubset(client_app_types) + + # Check grant control + built_in_controls = grant_controls.get("builtInControls", []) + grant_control = "block" if "block" in built_in_controls else "allow" + + policy_data.append({ + "id": policy.get("id"), + "display_name": policy.get("displayName"), + "state": policy.get("state"), + "targets_all_users": targets_all_users, + "targets_all_apps": targets_all_apps, + "blocks_legacy_auth": blocks_legacy, + "client_app_types": list(client_app_types), + "grant_control": grant_control, + }) + + return { + "conditional_access_policies": policy_data, + "total_policies": len(policy_data), + } diff --git a/engine/collectors/entra/devices/__init__.py b/engine/collectors/entra/devices/__init__.py new file mode 100644 index 00000000..826763c9 --- /dev/null +++ b/engine/collectors/entra/devices/__init__.py @@ -0,0 +1 @@ +"""Entra ID Devices collectors.""" diff --git a/engine/collectors/entra/devices/device_management_settings.py b/engine/collectors/entra/devices/device_management_settings.py new file mode 100644 index 00000000..837604fd --- /dev/null +++ b/engine/collectors/entra/devices/device_management_settings.py @@ -0,0 +1,50 @@ +"""Device management settings collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 4.1 + +Connection Method: Microsoft Graph API +Required Scopes: DeviceManagementConfiguration.Read.All +Graph Endpoint: /deviceManagement/settings +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class DeviceManagementSettingsDataCollector(BaseDataCollector): + """Collects Intune device management settings for CIS compliance evaluation. + + This collector retrieves Intune compliance policy default settings + to verify proper device management configuration. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect device management settings data. + + Returns: + Dict containing: + - device_management_settings: The device management settings + - compliance_policy_defaults: Default compliance policy settings + """ + # Get device management settings + settings = await client.get("/deviceManagement/settings", beta=True) + + # Get device compliance policy setting state summary + try: + compliance_settings = await client.get( + "/deviceManagement/deviceCompliancePolicySettingStateSummaries", + beta=True, + ) + except Exception: + compliance_settings = {} + + return { + "device_management_settings": settings, + "device_compliance_on_boarded": settings.get("deviceComplianceCheckinThresholdDays"), + "is_scheduled_action_enabled": settings.get("isScheduledActionEnabled"), + "secure_by_default": settings.get("secureByDefault"), + "compliance_policy_summaries": compliance_settings.get("value", []), + } diff --git a/engine/collectors/entra/devices/device_registration_policy.py b/engine/collectors/entra/devices/device_registration_policy.py new file mode 100644 index 00000000..e4f8f652 --- /dev/null +++ b/engine/collectors/entra/devices/device_registration_policy.py @@ -0,0 +1,54 @@ +"""Device registration policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.1.4.1, 5.1.4.2, 5.1.4.3, 5.1.4.4, 5.1.4.5 + +Connection Method: Microsoft Graph API +Required Scopes: Policy.Read.DeviceConfiguration +Graph Endpoint: /policies/deviceRegistrationPolicy +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class DeviceRegistrationPolicyDataCollector(BaseDataCollector): + """Collects device registration policy for CIS compliance evaluation. + + This collector retrieves device join settings, LAPS configuration, + and local admin assignment settings for compliance evaluation. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect device registration policy data. + + Returns: + Dict containing: + - device_registration_policy: The device registration policy + - azure_ad_join_settings: Azure AD join configuration + - local_admin_settings: Local admin assignment settings + - laps_settings: LAPS configuration + """ + # Get device registration policy + policy = await client.get("/policies/deviceRegistrationPolicy", beta=True) + + # Extract key settings + azure_ad_join = policy.get("azureADJoin", {}) + azure_ad_registration = policy.get("azureADRegistration", {}) + local_admin_password = policy.get("localAdminPassword", {}) + + return { + "device_registration_policy": policy, + "azure_ad_join_settings": azure_ad_join, + "azure_ad_join_allowed": azure_ad_join.get("isAdminConfigurable"), + "azure_ad_join_allowed_users": azure_ad_join.get("allowedUsers"), + "azure_ad_join_allowed_groups": azure_ad_join.get("allowedGroups"), + "azure_ad_registration_settings": azure_ad_registration, + "azure_ad_registration_allowed": azure_ad_registration.get("isAdminConfigurable"), + "local_admin_password_settings": local_admin_password, + "laps_enabled": local_admin_password.get("isEnabled"), + "user_device_quota": policy.get("userDeviceQuota"), + "multi_factor_auth_configuration": policy.get("multiFactorAuthConfiguration"), + } diff --git a/engine/collectors/entra/devices/enrollment_restrictions.py b/engine/collectors/entra/devices/enrollment_restrictions.py new file mode 100644 index 00000000..b5fe1ed2 --- /dev/null +++ b/engine/collectors/entra/devices/enrollment_restrictions.py @@ -0,0 +1,71 @@ +"""Enrollment restrictions collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 4.2 + +Connection Method: Microsoft Graph API +Required Scopes: DeviceManagementServiceConfig.Read.All +Graph Endpoint: /deviceManagement/deviceEnrollmentConfigurations +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class EnrollmentRestrictionsDataCollector(BaseDataCollector): + """Collects device enrollment restrictions for CIS compliance evaluation. + + This collector retrieves device enrollment restriction configurations + to verify personal device enrollment is properly restricted. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect enrollment restriction data. + + Returns: + Dict containing: + - enrollment_configurations: List of enrollment configurations + - platform_restrictions: Platform-specific enrollment restrictions + - personal_device_restrictions: Personal device enrollment settings + """ + # Get device enrollment configurations + configs = await client.get_all_pages( + "/deviceManagement/deviceEnrollmentConfigurations", + beta=True, + ) + + # Categorize configurations by type + platform_restrictions = [] + limit_restrictions = [] + other_configs = [] + + for config in configs: + config_type = config.get("@odata.type", "") + if "platformRestrictions" in config_type.lower(): + platform_restrictions.append(config) + elif "limit" in config_type.lower(): + limit_restrictions.append(config) + else: + other_configs.append(config) + + # Check if personal device enrollment is blocked + personal_devices_blocked = False + for restriction in platform_restrictions: + # Check platform restriction settings + platforms = ["androidRestriction", "iosRestriction", "windowsRestriction", "macOSRestriction"] + for platform in platforms: + platform_config = restriction.get(platform, {}) + if platform_config.get("personalDeviceEnrollmentBlocked"): + personal_devices_blocked = True + break + + return { + "enrollment_configurations": configs, + "total_configurations": len(configs), + "platform_restrictions": platform_restrictions, + "limit_restrictions": limit_restrictions, + "other_configurations": other_configs, + "personal_devices_blocked": personal_devices_blocked, + } diff --git a/engine/collectors/entra/domains/__init__.py b/engine/collectors/entra/domains/__init__.py new file mode 100644 index 00000000..da3bf032 --- /dev/null +++ b/engine/collectors/entra/domains/__init__.py @@ -0,0 +1 @@ +"""Entra ID domains collectors.""" diff --git a/engine/collectors/entra/domains/domains.py b/engine/collectors/entra/domains/domains.py new file mode 100644 index 00000000..79e97e67 --- /dev/null +++ b/engine/collectors/entra/domains/domains.py @@ -0,0 +1,53 @@ +"""Domains collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.3.1 + +Connection Method: Microsoft Graph API +Required Scopes: Domain.Read.All +Graph Endpoint: /domains +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class DomainsDataCollector(BaseDataCollector): + """Collects domain configuration for CIS compliance evaluation. + + This collector retrieves domain settings including password validity + period configuration needed for password expiration policy compliance. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect domain configuration data. + + Returns: + Dict containing: + - domains: List of domains with configuration details + - total_domains: Number of domains + - managed_domains_count: Number of managed (non-federated) domains + """ + # Get all domains + domains = await client.get_domains() + + # Categorize domains + verified_domains = [d for d in domains if d.get("isVerified")] + managed_domains = [d for d in domains if d.get("authenticationType") == "Managed"] + federated_domains = [d for d in domains if d.get("authenticationType") == "Federated"] + + return { + "domains": domains, + "total_domains": len(domains), + "verified_domains_count": len(verified_domains), + "managed_domains_count": len(managed_domains), + "federated_domains_count": len(federated_domains), + "default_domain": next( + (d for d in domains if d.get("isDefault")), None + ), + "initial_domain": next( + (d for d in domains if d.get("isInitial")), None + ), + } diff --git a/engine/collectors/entra/domains/password_policy.py b/engine/collectors/entra/domains/password_policy.py new file mode 100644 index 00000000..bff7b464 --- /dev/null +++ b/engine/collectors/entra/domains/password_policy.py @@ -0,0 +1,41 @@ +"""Password policy data collector. + +CIS-1.3.1: Ensure password expiration policy is set to never expire. +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class PasswordPolicyDataCollector(BaseDataCollector): + """Collects domain password expiration policy settings.""" + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect password policy for all domains. + + Returns: + Dict with domains list containing password policy details. + """ + domains = await client.get_all_pages("/domains") + + domain_data = [] + for domain in domains: + # Federated domains return null for password properties + is_managed = domain.get("authenticationType") == "Managed" + + domain_data.append({ + "domain_name": domain.get("id"), + "is_default": domain.get("isDefault", False), + "is_managed": is_managed, + "authentication_type": domain.get("authenticationType"), + "password_validity_days": domain.get("passwordValidityPeriodInDays"), + "password_notification_days": domain.get("passwordNotificationWindowInDays"), + }) + + return { + "domains": domain_data, + "total_domains": len(domain_data), + "managed_domains_count": sum(1 for d in domain_data if d["is_managed"]), + } diff --git a/engine/collectors/entra/governance/__init__.py b/engine/collectors/entra/governance/__init__.py new file mode 100644 index 00000000..99da1f43 --- /dev/null +++ b/engine/collectors/entra/governance/__init__.py @@ -0,0 +1 @@ +"""Entra ID Governance collectors.""" diff --git a/engine/collectors/entra/governance/access_reviews.py b/engine/collectors/entra/governance/access_reviews.py new file mode 100644 index 00000000..ed3ccb1d --- /dev/null +++ b/engine/collectors/entra/governance/access_reviews.py @@ -0,0 +1,55 @@ +"""Access reviews collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.3.2 + +Connection Method: Microsoft Graph API +Required Scopes: AccessReview.Read.All +Graph Endpoint: /identityGovernance/accessReviews/definitions +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class AccessReviewsDataCollector(BaseDataCollector): + """Collects access review definitions for CIS compliance evaluation. + + This collector retrieves access review configurations to verify + guest user access reviews are properly configured. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect access reviews data. + + Returns: + Dict containing: + - access_review_definitions: List of access review definitions + - total_reviews: Number of access review definitions + - guest_reviews: Access reviews for guest users + - guest_reviews_count: Number of guest user reviews + """ + # Get access review definitions + definitions = await client.get_all_pages( + "/identityGovernance/accessReviews/definitions", + beta=True, + ) + + # Filter for guest user reviews + guest_reviews = [] + for definition in definitions: + scope = definition.get("scope", {}) + # Check if review targets guest users + query = scope.get("query", "") + if "guest" in query.lower() or "userType eq 'Guest'" in query: + guest_reviews.append(definition) + + return { + "access_review_definitions": definitions, + "total_reviews": len(definitions), + "guest_reviews": guest_reviews, + "guest_reviews_count": len(guest_reviews), + "has_guest_reviews": len(guest_reviews) > 0, + } diff --git a/engine/collectors/entra/governance/pim_role_policies.py b/engine/collectors/entra/governance/pim_role_policies.py new file mode 100644 index 00000000..5e075d10 --- /dev/null +++ b/engine/collectors/entra/governance/pim_role_policies.py @@ -0,0 +1,180 @@ +"""PIM role policies collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.3.1, 5.3.3, 5.3.4, 5.3.5 + +Connection Method: Microsoft Graph API +Required Scopes: RoleManagementPolicy.Read.Directory +Graph Endpoints: + - /policies/roleManagementPolicies + - /policies/roleManagementPolicies/{id}/rules + - /policies/roleManagementPolicies/{id}/rules/Approval_EndUser_Assignment + +Controls covered: + - 5.3.1: Ensure 'Privileged Identity Management' is used to manage roles + - 5.3.3: Ensure 'Access reviews' for privileged roles are configured + - 5.3.4: Ensure approval is required for Global Administrator role activation + - 5.3.5: Ensure approval is required for Privileged Role Administrator activation +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class PimRolePoliciesDataCollector(BaseDataCollector): + """Collects PIM role management policies for CIS compliance evaluation. + + This collector retrieves Privileged Identity Management policies and rules + to verify proper configuration of privileged role management. + """ + + # Role IDs for privileged roles we need to check + GLOBAL_ADMIN_ROLE_TEMPLATE_ID = "62e90394-69f5-4237-9190-012177145e10" + PRIVILEGED_ROLE_ADMIN_TEMPLATE_ID = "e8611ab8-c189-46e8-94e1-60213ab1f814" + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect PIM role management policy data. + + Returns: + Dict containing: + - role_management_policies: List of all role management policies + - global_admin_policy: Policy for Global Administrator role with rules + - privileged_role_admin_policy: Policy for Privileged Role Administrator with rules + - global_admin_approval_required: Whether approval is required for GA activation + - privileged_role_admin_approval_required: Whether approval is required for PRA activation + - global_admin_mfa_required: Whether MFA is required for GA activation + - global_admin_justification_required: Whether justification is required for GA + - max_activation_duration_hours: Maximum activation duration configured + - pim_enabled: Whether PIM is being used (policies exist) + """ + # Step 1: Get all role management policies + # Note: This endpoint requires beta API for full policy details + policies_response = await client.get( + "/policies/roleManagementPolicies", + beta=True, + params={"$filter": "scopeId eq '/' and scopeType eq 'DirectoryRole'"}, + ) + policies = policies_response.get("value", []) + + # Step 2: Get role definitions to map scope to role names + role_definitions = await client.get_all_pages( + "/roleManagement/directory/roleDefinitions", + beta=True, + ) + role_map = {r.get("templateId"): r for r in role_definitions} + + # Step 3: Process each policy and enrich with rules for key roles + enriched_policies = [] + global_admin_policy = None + privileged_role_admin_policy = None + + for policy in policies: + policy_id = policy.get("id") + # scopeId format: /DirectoryRoles/{roleTemplateId} + scope_id = policy.get("scopeId", "") + + # Extract role template ID from scope + role_template_id = None + if scope_id.startswith("/"): + role_template_id = scope_id.split("/")[-1] if "/" in scope_id else scope_id + + policy_data = { + "id": policy_id, + "scopeId": scope_id, + "scopeType": policy.get("scopeType"), + "roleTemplateId": role_template_id, + "roleName": role_map.get(role_template_id, {}).get("displayName"), + } + + # Get detailed rules for Global Admin and Privileged Role Admin + if role_template_id in [ + self.GLOBAL_ADMIN_ROLE_TEMPLATE_ID, + self.PRIVILEGED_ROLE_ADMIN_TEMPLATE_ID, + ]: + rules_response = await client.get( + f"/policies/roleManagementPolicies/{policy_id}/rules", + beta=True, + ) + rules = rules_response.get("value", []) + policy_data["rules"] = rules + + # Extract key settings from rules + policy_data.update(self._extract_rule_settings(rules)) + + if role_template_id == self.GLOBAL_ADMIN_ROLE_TEMPLATE_ID: + global_admin_policy = policy_data + elif role_template_id == self.PRIVILEGED_ROLE_ADMIN_TEMPLATE_ID: + privileged_role_admin_policy = policy_data + + enriched_policies.append(policy_data) + + return { + "role_management_policies": enriched_policies, + "total_policies": len(policies), + "pim_enabled": len(policies) > 0, + "global_admin_policy": global_admin_policy, + "privileged_role_admin_policy": privileged_role_admin_policy, + "global_admin_approval_required": ( + global_admin_policy.get("approval_required") if global_admin_policy else None + ), + "privileged_role_admin_approval_required": ( + privileged_role_admin_policy.get("approval_required") + if privileged_role_admin_policy + else None + ), + "global_admin_mfa_required": ( + global_admin_policy.get("mfa_required") if global_admin_policy else None + ), + "global_admin_justification_required": ( + global_admin_policy.get("justification_required") if global_admin_policy else None + ), + "global_admin_max_activation_duration": ( + global_admin_policy.get("max_activation_duration") if global_admin_policy else None + ), + } + + def _extract_rule_settings(self, rules: list[dict]) -> dict[str, Any]: + """Extract key settings from PIM policy rules. + + Args: + rules: List of rule objects from roleManagementPolicies/{id}/rules + + Returns: + Dict with extracted settings: + - approval_required: bool + - mfa_required: bool + - justification_required: bool + - max_activation_duration: str (ISO 8601 duration) + """ + settings = { + "approval_required": None, + "mfa_required": None, + "justification_required": None, + "max_activation_duration": None, + } + + for rule in rules: + rule_type = rule.get("@odata.type", "") + rule_id = rule.get("id", "") + + # Approval rule + if "unifiedRoleManagementPolicyApprovalRule" in rule_type: + if "EndUser_Assignment" in rule_id: + setting = rule.get("setting", {}) + settings["approval_required"] = setting.get("isApprovalRequired", False) + + # Enablement rule (MFA and justification) + elif "unifiedRoleManagementPolicyEnablementRule" in rule_type: + if "EndUser_Assignment" in rule_id: + enabled_rules = rule.get("enabledRules", []) + settings["mfa_required"] = "MultiFactorAuthentication" in enabled_rules + settings["justification_required"] = "Justification" in enabled_rules + + # Expiration rule (max duration) + elif "unifiedRoleManagementPolicyExpirationRule" in rule_type: + if "EndUser_Assignment" in rule_id: + settings["max_activation_duration"] = rule.get("maximumDuration") + + return settings diff --git a/engine/collectors/entra/groups/__init__.py b/engine/collectors/entra/groups/__init__.py new file mode 100644 index 00000000..cb6af48f --- /dev/null +++ b/engine/collectors/entra/groups/__init__.py @@ -0,0 +1 @@ +"""Entra ID Groups collectors.""" diff --git a/engine/collectors/entra/groups/groups.py b/engine/collectors/entra/groups/groups.py new file mode 100644 index 00000000..f872b183 --- /dev/null +++ b/engine/collectors/entra/groups/groups.py @@ -0,0 +1,69 @@ +"""Groups collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.2.1, 5.1.3.1, 5.1.3.2 + +Connection Method: Microsoft Graph API +Required Scopes: Group.Read.All +Graph Endpoint: /groups +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class GroupsDataCollector(BaseDataCollector): + """Collects group information for CIS compliance evaluation. + + This collector retrieves group details including dynamic groups, + public groups, and group settings needed for compliance evaluation. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect group data. + + Returns: + Dict containing: + - groups: List of groups with properties + - total_groups: Number of groups + - dynamic_groups_count: Number of dynamic membership groups + - public_groups_count: Number of public groups + """ + # Get all groups with relevant properties + groups = await client.get_all_pages( + "/groups", + params={ + "$select": "id,displayName,groupTypes,membershipRule,visibility,securityEnabled,mailEnabled" + }, + ) + + # Categorize groups + dynamic_groups = [ + g for g in groups + if "DynamicMembership" in g.get("groupTypes", []) + ] + public_groups = [ + g for g in groups + if g.get("visibility") == "Public" + ] + security_groups = [ + g for g in groups + if g.get("securityEnabled") + ] + m365_groups = [ + g for g in groups + if "Unified" in g.get("groupTypes", []) + ] + + return { + "groups": groups, + "total_groups": len(groups), + "dynamic_groups": dynamic_groups, + "dynamic_groups_count": len(dynamic_groups), + "public_groups": public_groups, + "public_groups_count": len(public_groups), + "security_groups_count": len(security_groups), + "m365_groups_count": len(m365_groups), + } diff --git a/engine/collectors/entra/policies/__init__.py b/engine/collectors/entra/policies/__init__.py new file mode 100644 index 00000000..7e74674e --- /dev/null +++ b/engine/collectors/entra/policies/__init__.py @@ -0,0 +1 @@ +"""Entra ID Policies collectors.""" diff --git a/engine/collectors/entra/policies/activity_timeout_policy.py b/engine/collectors/entra/policies/activity_timeout_policy.py new file mode 100644 index 00000000..2ea0dd56 --- /dev/null +++ b/engine/collectors/entra/policies/activity_timeout_policy.py @@ -0,0 +1,56 @@ +"""Activity timeout policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.3.2 + +Connection Method: Microsoft Graph API +Required Scopes: Policy.Read.All +Graph Endpoint: /policies/activityBasedTimeoutPolicies +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class ActivityTimeoutPolicyDataCollector(BaseDataCollector): + """Collects activity-based timeout policy for CIS compliance evaluation. + + This collector retrieves idle session timeout configuration settings + for compliance with session management requirements. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect activity timeout policy data. + + Returns: + Dict containing: + - timeout_policies: List of activity-based timeout policies + - has_timeout_policy: Whether a timeout policy is configured + """ + # Get activity-based timeout policies + policies = await client.get_all_pages( + "/policies/activityBasedTimeoutPolicies", + ) + + # Parse the policy definition to extract timeout values + timeout_settings = [] + for policy in policies: + definition = policy.get("definition", []) + if definition: + # Definition is a JSON string array + import json + for def_str in definition: + try: + parsed = json.loads(def_str) + timeout_settings.append(parsed) + except (json.JSONDecodeError, TypeError): + pass + + return { + "timeout_policies": policies, + "total_policies": len(policies), + "has_timeout_policy": len(policies) > 0, + "timeout_settings": timeout_settings, + } diff --git a/engine/collectors/entra/policies/admin_consent_request_policy.py b/engine/collectors/entra/policies/admin_consent_request_policy.py new file mode 100644 index 00000000..dddd667a --- /dev/null +++ b/engine/collectors/entra/policies/admin_consent_request_policy.py @@ -0,0 +1,47 @@ +"""Admin consent request policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.1.5.2 + +Connection Method: Microsoft Graph API +Required Scopes: Policy.Read.All +Graph Endpoint: /policies/adminConsentRequestPolicy +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class AdminConsentRequestPolicyDataCollector(BaseDataCollector): + """Collects admin consent workflow settings for CIS compliance evaluation. + + This collector retrieves the admin consent request policy configuration + to verify admin consent workflow is properly configured. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect admin consent request policy data. + + Returns: + Dict containing: + - admin_consent_policy: The admin consent request policy + - is_enabled: Whether admin consent workflow is enabled + - reviewers: List of designated reviewers + """ + # Get admin consent request policy + policy = await client.get("/policies/adminConsentRequestPolicy", beta=True) + + # Extract reviewer information + reviewers = policy.get("reviewers", []) + + return { + "admin_consent_policy": policy, + "is_enabled": policy.get("isEnabled"), + "notify_reviewers": policy.get("notifyReviewers"), + "reminders_enabled": policy.get("remindersEnabled"), + "request_duration_in_days": policy.get("requestDurationInDays"), + "reviewers": reviewers, + "reviewers_count": len(reviewers), + } diff --git a/engine/collectors/entra/policies/authorization_policy.py b/engine/collectors/entra/policies/authorization_policy.py new file mode 100644 index 00000000..7b60b774 --- /dev/null +++ b/engine/collectors/entra/policies/authorization_policy.py @@ -0,0 +1,51 @@ +"""Authorization policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.1.2.2, 5.1.2.3, 5.1.3.2, 5.1.4.6, 5.1.5.1, 5.1.6.2, 5.1.6.3 + +Connection Method: Microsoft Graph API +Required Scopes: Policy.Read.All +Graph Endpoint: /policies/authorizationPolicy +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class AuthorizationPolicyDataCollector(BaseDataCollector): + """Collects authorization policy settings for CIS compliance evaluation. + + This collector retrieves the authorization policy which contains default + user role permissions, guest settings, and invitation settings. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect authorization policy data. + + Returns: + Dict containing: + - authorization_policy: The authorization policy configuration + - default_user_role_permissions: Default permissions for users + - guest_invite_settings: Guest invitation settings + """ + # Get authorization policy + policy = await client.get("/policies/authorizationPolicy") + + # Extract default user role permissions + default_permissions = policy.get("defaultUserRolePermissions", {}) + + return { + "authorization_policy": policy, + "default_user_role_permissions": default_permissions, + "allowed_to_create_apps": default_permissions.get("allowedToCreateApps"), + "allowed_to_create_security_groups": default_permissions.get("allowedToCreateSecurityGroups"), + "allowed_to_create_tenants": default_permissions.get("allowedToCreateTenants"), + "allowed_to_read_bitlocker_keys_for_owned_device": default_permissions.get("allowedToReadBitlockerKeysForOwnedDevice"), + "allowed_to_read_other_users": default_permissions.get("allowedToReadOtherUsers"), + "guest_user_role_id": policy.get("guestUserRoleId"), + "allow_invites_from": policy.get("allowInvitesFrom"), + "allow_email_verified_users_to_join_organization": policy.get("allowEmailVerifiedUsersToJoinOrganization"), + "block_msol_power_shell": policy.get("blockMsolPowerShell"), + } diff --git a/engine/collectors/entra/policies/b2b_policy.py b/engine/collectors/entra/policies/b2b_policy.py new file mode 100644 index 00000000..1a9d17b6 --- /dev/null +++ b/engine/collectors/entra/policies/b2b_policy.py @@ -0,0 +1,55 @@ +"""B2B collaboration policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 5.1.6.1 + +Connection Method: Microsoft Graph API +Required Scopes: Policy.Read.All +Graph Endpoint: /policies/crossTenantAccessPolicy/default +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class B2BPolicyDataCollector(BaseDataCollector): + """Collects B2B collaboration policy for CIS compliance evaluation. + + This collector retrieves B2B collaboration settings including domain + allowlist/blocklist configuration for external collaboration. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect B2B collaboration policy data. + + Returns: + Dict containing: + - b2b_policy: The B2B collaboration policy configuration + - allowed_domains: List of allowed domains (if allowlist) + - blocked_domains: List of blocked domains (if blocklist) + - restriction_mode: The domain restriction mode + """ + # Get cross-tenant access policy default settings + policy = await client.get("/policies/crossTenantAccessPolicy/default", beta=True) + + # Extract B2B collaboration settings + b2b_collaboration = policy.get("b2bCollaborationInbound", {}) + b2b_direct_connect = policy.get("b2bDirectConnectInbound", {}) + + # Get cross-tenant access policy partners for domain restrictions + partners_response = await client.get("/policies/crossTenantAccessPolicy/partners", beta=True) + partners = partners_response.get("value", []) + + return { + "cross_tenant_access_policy": policy, + "b2b_collaboration_inbound": b2b_collaboration, + "b2b_collaboration_outbound": policy.get("b2bCollaborationOutbound", {}), + "b2b_direct_connect_inbound": b2b_direct_connect, + "b2b_direct_connect_outbound": policy.get("b2bDirectConnectOutbound", {}), + "inbound_trust": policy.get("inboundTrust", {}), + "partners": partners, + "partners_count": len(partners), + "is_service_provider": policy.get("isServiceProvider"), + } diff --git a/engine/collectors/entra/roles/__init__.py b/engine/collectors/entra/roles/__init__.py new file mode 100644 index 00000000..89930d08 --- /dev/null +++ b/engine/collectors/entra/roles/__init__.py @@ -0,0 +1 @@ +"""Entra ID role and admin collectors.""" diff --git a/engine/collectors/entra/roles/cloud_only_admins.py b/engine/collectors/entra/roles/cloud_only_admins.py new file mode 100644 index 00000000..b5680370 --- /dev/null +++ b/engine/collectors/entra/roles/cloud_only_admins.py @@ -0,0 +1,101 @@ +"""Cloud-only admins data collector. + +CIS-1.1.1: Ensure Administrative accounts are cloud-only. +Administrative accounts should not be synced from on-premises Active Directory. +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class CloudOnlyAdminsDataCollector(BaseDataCollector): + """Collects admin accounts and checks if they are cloud-only.""" + + # Roles that are considered administrative + ADMIN_ROLE_NAMES = [ + "Global Administrator", + "Privileged Role Administrator", + "Privileged Authentication Administrator", + "Security Administrator", + "Exchange Administrator", + "SharePoint Administrator", + "Intune Administrator", + "Application Administrator", + "Cloud Application Administrator", + "Azure AD Joined Device Local Administrator", + "Compliance Administrator", + "Conditional Access Administrator", + "User Administrator", + ] + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect admin account information and check for on-prem sync. + + Returns: + Dict with admin_accounts list, each containing: + - userPrincipalName + - displayName + - on_premises_sync_enabled + - admin_roles (list of roles the user holds) + """ + # Get all directory roles + roles = await client.get_directory_roles() + + # Find admin roles by name + admin_roles = [ + role for role in roles if role.get("displayName") in self.ADMIN_ROLE_NAMES + ] + + # Collect all admin users (deduplicated) + admin_users: dict[str, dict[str, Any]] = {} + + for role in admin_roles: + role_name = role.get("displayName", "Unknown") + members = await client.get_role_members(role["id"]) + + for member in members: + # Only process user objects + if member.get("@odata.type") != "#microsoft.graph.user": + continue + + user_id = member.get("id") + if not user_id: + continue + + if user_id in admin_users: + # Add role to existing user + admin_users[user_id]["admin_roles"].append(role_name) + else: + # Get full user details including onPremisesSyncEnabled + user_details = await client.get( + f"/users/{user_id}", + params={ + "$select": "id,userPrincipalName,displayName,onPremisesSyncEnabled" + }, + ) + + admin_users[user_id] = { + "id": user_id, + "userPrincipalName": user_details.get("userPrincipalName"), + "displayName": user_details.get("displayName"), + "on_premises_sync_enabled": user_details.get( + "onPremisesSyncEnabled", False + ) + or False, + "admin_roles": [role_name], + } + + admin_accounts = list(admin_users.values()) + + return { + "admin_accounts": admin_accounts, + "total_admin_accounts": len(admin_accounts), + "synced_admin_count": sum( + 1 for a in admin_accounts if a["on_premises_sync_enabled"] + ), + "cloud_only_admin_count": sum( + 1 for a in admin_accounts if not a["on_premises_sync_enabled"] + ), + } diff --git a/engine/collectors/entra/roles/directory_roles.py b/engine/collectors/entra/roles/directory_roles.py new file mode 100644 index 00000000..e2206f52 --- /dev/null +++ b/engine/collectors/entra/roles/directory_roles.py @@ -0,0 +1,76 @@ +"""Directory roles collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.1.1, 1.1.3, 1.1.4 + +Connection Method: Microsoft Graph API +Required Scopes: RoleManagement.Read.Directory, User.Read.All +Graph Endpoint: /directoryRoles, /directoryRoles/{id}/members, /users/{id} +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class DirectoryRolesDataCollector(BaseDataCollector): + """Collects directory role assignments for CIS compliance evaluation. + + This collector retrieves all directory roles and their members, including + user properties needed to evaluate administrative account compliance. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect directory roles and member information. + + Returns: + Dict containing: + - directory_roles: List of roles with their members + - total_roles: Number of directory roles + - admin_users: Deduplicated list of users with admin roles + """ + # Get all directory roles + roles = await client.get_directory_roles() + + # For each role, get members + roles_with_members = [] + admin_users: dict[str, dict] = {} + + for role in roles: + role_id = role.get("id") + role_name = role.get("displayName", "") + + # Get members of this role + members = await client.get_role_members(role_id) + + role_data = { + "id": role_id, + "displayName": role_name, + "description": role.get("description"), + "roleTemplateId": role.get("roleTemplateId"), + "members": members, + "members_count": len(members), + } + roles_with_members.append(role_data) + + # Collect unique admin users + for member in members: + if member.get("@odata.type") == "#microsoft.graph.user": + user_id = member.get("id") + if user_id not in admin_users: + admin_users[user_id] = { + "id": user_id, + "displayName": member.get("displayName"), + "userPrincipalName": member.get("userPrincipalName"), + "roles": [role_name], + } + else: + admin_users[user_id]["roles"].append(role_name) + + return { + "directory_roles": roles_with_members, + "total_roles": len(roles_with_members), + "admin_users": list(admin_users.values()), + "admin_users_count": len(admin_users), + } diff --git a/engine/collectors/entra/roles/privileged_roles.py b/engine/collectors/entra/roles/privileged_roles.py new file mode 100644 index 00000000..bac98ba0 --- /dev/null +++ b/engine/collectors/entra/roles/privileged_roles.py @@ -0,0 +1,62 @@ +"""Privileged roles data collector. + +CIS-1.1.3: Ensure that between two and four global admins are designated. +Maintain 2-4 global administrators for operational continuity and security. +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class PrivilegedRolesDataCollector(BaseDataCollector): + """Collects global admin count and members.""" + + GLOBAL_ADMIN_ROLE_NAME = "Global Administrator" + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect global admin information. + + Returns: + Dict with: + - global_admin_count: Number of global admins + - global_admins: List of UPNs + - global_admin_details: List of admin details (id, UPN, displayName) + """ + # Get all directory roles + roles = await client.get_directory_roles() + + # Find Global Administrator role + global_admin_role = None + for role in roles: + if role.get("displayName") == self.GLOBAL_ADMIN_ROLE_NAME: + global_admin_role = role + break + + if not global_admin_role: + return { + "global_admin_count": 0, + "global_admins": [], + "global_admin_details": [], + "error": "Global Administrator role not found", + } + + # Get members of the Global Administrator role + members = await client.get_role_members(global_admin_role["id"]) + + global_admins = [ + { + "id": m.get("id"), + "userPrincipalName": m.get("userPrincipalName"), + "displayName": m.get("displayName"), + } + for m in members + if m.get("@odata.type") == "#microsoft.graph.user" + ] + + return { + "global_admin_count": len(global_admins), + "global_admins": [ga["userPrincipalName"] for ga in global_admins], + "global_admin_details": global_admins, + } diff --git a/engine/collectors/entra/users/__init__.py b/engine/collectors/entra/users/__init__.py new file mode 100644 index 00000000..9aed3bd8 --- /dev/null +++ b/engine/collectors/entra/users/__init__.py @@ -0,0 +1 @@ +"""Entra ID Users collectors.""" diff --git a/engine/collectors/entra/users/users.py b/engine/collectors/entra/users/users.py new file mode 100644 index 00000000..07a678b4 --- /dev/null +++ b/engine/collectors/entra/users/users.py @@ -0,0 +1,58 @@ +"""Users collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.1.1, 1.1.4, 5.2.3.4 + +Connection Method: Microsoft Graph API +Required Scopes: User.Read.All +Graph Endpoint: /users +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class UsersDataCollector(BaseDataCollector): + """Collects user information for CIS compliance evaluation. + + This collector retrieves user properties including sync status, + license assignments, and account types needed for compliance evaluation. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect user data. + + Returns: + Dict containing: + - users: List of users with properties + - total_users: Number of users + - synced_users_count: Number of users synced from on-premises + - cloud_only_users_count: Number of cloud-only users + """ + # Get all users with relevant properties + users = await client.get_all_pages( + "/users", + params={ + "$select": "id,userPrincipalName,displayName,accountEnabled,userType,onPremisesSyncEnabled,assignedLicenses" + }, + ) + + # Categorize users + synced_users = [u for u in users if u.get("onPremisesSyncEnabled")] + cloud_only_users = [u for u in users if not u.get("onPremisesSyncEnabled")] + guest_users = [u for u in users if u.get("userType") == "Guest"] + member_users = [u for u in users if u.get("userType") == "Member"] + disabled_users = [u for u in users if not u.get("accountEnabled")] + + return { + "users": users, + "total_users": len(users), + "synced_users_count": len(synced_users), + "cloud_only_users_count": len(cloud_only_users), + "guest_users_count": len(guest_users), + "member_users_count": len(member_users), + "disabled_users_count": len(disabled_users), + "enabled_users_count": len(users) - len(disabled_users), + } diff --git a/engine/collectors/exchange/__init__.py b/engine/collectors/exchange/__init__.py new file mode 100644 index 00000000..d4caa101 --- /dev/null +++ b/engine/collectors/exchange/__init__.py @@ -0,0 +1,6 @@ +"""Exchange Online collectors. + +These collectors use Exchange Online PowerShell via the ExchangeOnlineManagement module. +Authentication uses client secret via MSAL to obtain access tokens, +which are passed to PowerShell cmdlets via the -AccessToken parameter. +""" diff --git a/engine/collectors/exchange/audit/__init__.py b/engine/collectors/exchange/audit/__init__.py new file mode 100644 index 00000000..2c164b37 --- /dev/null +++ b/engine/collectors/exchange/audit/__init__.py @@ -0,0 +1 @@ +"""Exchange Online Audit collectors.""" diff --git a/engine/collectors/exchange/audit/admin_audit_log_config.py b/engine/collectors/exchange/audit/admin_audit_log_config.py new file mode 100644 index 00000000..4f89b2fb --- /dev/null +++ b/engine/collectors/exchange/audit/admin_audit_log_config.py @@ -0,0 +1,38 @@ +"""Admin audit log config collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 3.1.1 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-AdminAuditLogConfig +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class AdminAuditLogConfigDataCollector(BasePowerShellCollector): + """Collects admin audit log configuration for CIS compliance evaluation. + + This collector retrieves unified audit log ingestion status + to verify audit logging is properly enabled. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect admin audit log config data. + + Returns: + Dict containing: + - admin_audit_log_config: The full admin audit log configuration + - unified_audit_log_ingestion_enabled: UAL ingestion status (key for CIS 3.1.1) + """ + config = await client.run_cmdlet("ExchangeOnline", "Get-AdminAuditLogConfig") + + return { + "admin_audit_log_config": config, + "unified_audit_log_ingestion_enabled": config.get("UnifiedAuditLogIngestionEnabled"), + } diff --git a/engine/collectors/exchange/authentication/__init__.py b/engine/collectors/exchange/authentication/__init__.py new file mode 100644 index 00000000..47a9d930 --- /dev/null +++ b/engine/collectors/exchange/authentication/__init__.py @@ -0,0 +1 @@ +"""Exchange Online Authentication collectors.""" diff --git a/engine/collectors/exchange/authentication/dkim_signing_config.py b/engine/collectors/exchange/authentication/dkim_signing_config.py new file mode 100644 index 00000000..d01fd0b7 --- /dev/null +++ b/engine/collectors/exchange/authentication/dkim_signing_config.py @@ -0,0 +1,50 @@ +"""DKIM signing config collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.9 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-DkimSigningConfig +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class DkimSigningConfigDataCollector(BasePowerShellCollector): + """Collects DKIM signing configuration for CIS compliance evaluation. + + This collector retrieves DKIM signing status for all domains + to verify email authentication is properly configured. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect DKIM signing config data. + + Returns: + Dict containing: + - dkim_configs: List of DKIM configurations per domain + - domains_with_dkim_enabled: Domains with DKIM enabled + - domains_with_dkim_disabled: Domains without DKIM enabled + """ + configs = await client.run_cmdlet("ExchangeOnline", "Get-DkimSigningConfig") + + # Handle None, single config, or list + if configs is None: + configs = [] + elif isinstance(configs, dict): + configs = [configs] + + domains_enabled = [c.get("Domain") for c in configs if c.get("Enabled")] + domains_disabled = [c.get("Domain") for c in configs if not c.get("Enabled")] + + return { + "dkim_configs": configs, + "total_domains": len(configs), + "domains_with_dkim_enabled": domains_enabled, + "domains_with_dkim_disabled": domains_disabled, + } diff --git a/engine/collectors/exchange/dns/__init__.py b/engine/collectors/exchange/dns/__init__.py new file mode 100644 index 00000000..2fc0fcc6 --- /dev/null +++ b/engine/collectors/exchange/dns/__init__.py @@ -0,0 +1 @@ +"""Exchange DNS collectors.""" diff --git a/engine/collectors/exchange/dns/dns_security_records.py b/engine/collectors/exchange/dns/dns_security_records.py new file mode 100644 index 00000000..d2e41b72 --- /dev/null +++ b/engine/collectors/exchange/dns/dns_security_records.py @@ -0,0 +1,132 @@ +"""DNS security records collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.8, 2.1.10 + +Control Descriptions: + 2.1.8 - Ensure that SPF records are published for all Exchange Domains + 2.1.10 - Ensure DMARC Records for all Exchange Online domains are published + +Connection Method: Microsoft Graph API + DNS queries +Required Scopes: Domain.Read.All +Graph Endpoint: /domains +DNS Records: SPF (TXT), DMARC (_dmarc.{domain} TXT) + +Note: This collector uses a two-step approach: + 1. Retrieve tenant domains via Microsoft Graph API + 2. Query DNS for SPF and DMARC records for each verified domain +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.graph_client import GraphClient + + +class DnsSecurityRecordsDataCollector(BaseDataCollector): + """Collects DNS security records (SPF, DMARC) for CIS compliance evaluation. + + This collector retrieves all verified domains from the tenant via Graph API, + then performs DNS lookups for SPF and DMARC records on each domain. + """ + + async def collect(self, client: GraphClient) -> dict[str, Any]: + """Collect DNS security records for all tenant domains. + + Returns: + Dict containing: + - domains: List of domain records with SPF/DMARC data + - total_domains: Number of verified domains checked + """ + import dns.resolver + + # Step 1: Get domains from Graph API + domains = await client.get_domains() + + # Step 2: Filter for verified domains + verified_domains = [d for d in domains if d.get("isVerified", False)] + + domain_records = [] + for domain in verified_domains: + domain_id = domain.get("id") + + record = { + "domain": domain_id, + "is_verified": True, + "is_default": domain.get("isDefault", False), + "is_initial": domain.get("isInitial", False), + "authentication_type": domain.get("authenticationType"), + "spf_record": None, + "dmarc_record": None, + "dmarc_policy": None, + "spf_error": None, + "dmarc_error": None, + } + + # Query SPF record (TXT record at domain root) + try: + answers = dns.resolver.resolve(domain_id, "TXT") + for rdata in answers: + # TXT records may have multiple strings, join them + txt_value = "".join(s.decode() if isinstance(s, bytes) else s for s in rdata.strings) + if txt_value.startswith("v=spf1"): + record["spf_record"] = txt_value + break + except dns.resolver.NXDOMAIN: + record["spf_error"] = "Domain not found" + except dns.resolver.NoAnswer: + record["spf_error"] = "No TXT records" + except dns.resolver.NoNameservers: + record["spf_error"] = "No nameservers available" + except dns.resolver.Timeout: + record["spf_error"] = "DNS query timeout" + except Exception as e: + record["spf_error"] = str(e) + + # Query DMARC record (TXT record at _dmarc.{domain}) + dmarc_domain = f"_dmarc.{domain_id}" + try: + answers = dns.resolver.resolve(dmarc_domain, "TXT") + for rdata in answers: + txt_value = "".join(s.decode() if isinstance(s, bytes) else s for s in rdata.strings) + if txt_value.startswith("v=DMARC1"): + record["dmarc_record"] = txt_value + # Parse DMARC policy + record["dmarc_policy"] = self._parse_dmarc_policy(txt_value) + break + except dns.resolver.NXDOMAIN: + record["dmarc_error"] = "DMARC record not found" + except dns.resolver.NoAnswer: + record["dmarc_error"] = "No DMARC TXT record" + except dns.resolver.NoNameservers: + record["dmarc_error"] = "No nameservers available" + except dns.resolver.Timeout: + record["dmarc_error"] = "DNS query timeout" + except Exception as e: + record["dmarc_error"] = str(e) + + domain_records.append(record) + + return { + "domains": domain_records, + "total_domains": len(domain_records), + } + + def _parse_dmarc_policy(self, dmarc_record: str) -> dict[str, str]: + """Parse DMARC record into key-value pairs. + + Args: + dmarc_record: Raw DMARC TXT record value + + Returns: + Dict with DMARC policy settings (p, sp, rua, ruf, pct, etc.) + """ + policy = {} + # Split by semicolons and parse key=value pairs + parts = dmarc_record.split(";") + for part in parts: + part = part.strip() + if "=" in part: + key, value = part.split("=", 1) + policy[key.strip()] = value.strip() + return policy diff --git a/engine/collectors/exchange/mailbox/__init__.py b/engine/collectors/exchange/mailbox/__init__.py new file mode 100644 index 00000000..1ca1717e --- /dev/null +++ b/engine/collectors/exchange/mailbox/__init__.py @@ -0,0 +1 @@ +"""Exchange Online Mailbox collectors.""" diff --git a/engine/collectors/exchange/mailbox/mailbox_audit.py b/engine/collectors/exchange/mailbox/mailbox_audit.py new file mode 100644 index 00000000..7888353a --- /dev/null +++ b/engine/collectors/exchange/mailbox/mailbox_audit.py @@ -0,0 +1,54 @@ +"""Mailbox audit bypass collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 6.1.3 + +Control Description: + 6.1.3 - Ensure mailbox audit logging bypass is not enabled + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-MailboxAuditBypassAssociation +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class MailboxAuditDataCollector(BasePowerShellCollector): + """Collects mailbox audit bypass associations for CIS 6.1.3 evaluation. + + This collector retrieves mailboxes that have audit bypass enabled. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect mailbox audit bypass data. + + Returns: + Dict containing: + - accounts_with_bypass_enabled: List of accounts with AuditBypassEnabled = True + - bypass_count: Number of accounts with bypass enabled + """ + # Get only mailboxes with AuditBypassEnabled = True + # Filter in PowerShell to avoid large output and suppress warnings + cmdlet = ( + "Get-MailboxAuditBypassAssociation -ResultSize Unlimited " + "-WarningAction SilentlyContinue | " + "Where-Object { $_.AuditBypassEnabled -eq $true } | " + "Select-Object Name, AuditBypassEnabled" + ) + bypassed = await client.run_cmdlet("ExchangeOnline", cmdlet) + + # Handle None, single result, or list + if bypassed is None: + bypassed = [] + elif isinstance(bypassed, dict): + bypassed = [bypassed] + + return { + "accounts_with_bypass_enabled": bypassed, + "bypass_count": len(bypassed), + } diff --git a/engine/collectors/exchange/mailbox/mailbox_audit_actions.py b/engine/collectors/exchange/mailbox/mailbox_audit_actions.py new file mode 100644 index 00000000..109c45b1 --- /dev/null +++ b/engine/collectors/exchange/mailbox/mailbox_audit_actions.py @@ -0,0 +1,55 @@ +"""Mailbox audit actions collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 6.1.2 + +Control Description: + 6.1.2 - Ensure mailbox auditing for all users is Enabled + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-EXOMailbox +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class MailboxAuditActionsDataCollector(BasePowerShellCollector): + """Collects mailbox audit action configuration for CIS 6.1.2 evaluation. + + This collector retrieves audit action settings (AuditAdmin, AuditDelegate, + AuditOwner) for user mailboxes. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect mailbox audit action configuration. + + Returns: + Dict containing: + - mailboxes: List of user mailboxes with audit settings + - total_user_mailboxes: Total number of user mailboxes + """ + # Get user mailboxes with audit properties + # Filter in PowerShell and select only needed properties to reduce output + cmdlet = ( + "Get-EXOMailbox -PropertySets Audit, Minimum -ResultSize Unlimited " + "-WarningAction SilentlyContinue | " + "Where-Object { $_.RecipientTypeDetails -eq 'UserMailbox' } | " + "Select-Object UserPrincipalName, AuditEnabled, AuditAdmin, AuditDelegate, AuditOwner" + ) + mailboxes = await client.run_cmdlet("ExchangeOnline", cmdlet) + + # Handle None, single result, or list + if mailboxes is None: + mailboxes = [] + elif isinstance(mailboxes, dict): + mailboxes = [mailboxes] + + return { + "mailboxes": mailboxes, + "total_user_mailboxes": len(mailboxes), + } diff --git a/engine/collectors/exchange/mailbox/mailboxes.py b/engine/collectors/exchange/mailbox/mailboxes.py new file mode 100644 index 00000000..00074052 --- /dev/null +++ b/engine/collectors/exchange/mailbox/mailboxes.py @@ -0,0 +1,50 @@ +"""Mailboxes collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.2.2 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-EXOMailbox +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class MailboxesDataCollector(BasePowerShellCollector): + """Collects mailbox information for CIS compliance evaluation. + + This collector retrieves shared mailboxes to verify + shared mailboxes have appropriate sign-in settings. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect mailbox data. + + Returns: + Dict containing: + - shared_mailboxes: List of shared mailboxes + - total_shared_mailboxes: Count of shared mailboxes + """ + # Get shared mailboxes only (RecipientTypeDetails -eq 'SharedMailbox') + mailboxes = await client.run_cmdlet( + "ExchangeOnline", + "Get-EXOMailbox", + RecipientTypeDetails="SharedMailbox", + ResultSize="Unlimited", + ) + + # Handle None, single result, or list + if mailboxes is None: + mailboxes = [] + elif isinstance(mailboxes, dict): + mailboxes = [mailboxes] + + return { + "shared_mailboxes": mailboxes, + "total_shared_mailboxes": len(mailboxes), + } diff --git a/engine/collectors/exchange/mailbox/role_assignment_policy.py b/engine/collectors/exchange/mailbox/role_assignment_policy.py new file mode 100644 index 00000000..36844731 --- /dev/null +++ b/engine/collectors/exchange/mailbox/role_assignment_policy.py @@ -0,0 +1,67 @@ +"""Role assignment policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 6.3.1 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-RoleAssignmentPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class RoleAssignmentPolicyDataCollector(BasePowerShellCollector): + """Collects role assignment policy for CIS compliance evaluation. + + This collector retrieves Outlook add-in installation permissions + configured in role assignment policies. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect role assignment policy data. + + Returns: + Dict containing: + - role_assignment_policies: List of role assignment policies + - default_policy: The default role assignment policy + - policies_allowing_addin_install: Policies that allow add-in installation + """ + policies = await client.run_cmdlet("ExchangeOnline", "Get-RoleAssignmentPolicy") + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Find default policy + default_policy = next( + (p for p in policies if p.get("IsDefault")), + policies[0] if policies else None + ) + + # Check which policies allow add-in installation + # The assigned roles contain entries like "My Custom Apps", "My Marketplace Apps", "My ReadWriteMailbox Apps" + policies_allowing_addins = [] + for policy in policies: + assigned_roles = policy.get("AssignedRoles", []) + # Check for add-in related roles + addin_roles = [r for r in assigned_roles if "Apps" in r or "Add-In" in r] + if addin_roles: + policies_allowing_addins.append({ + "name": policy.get("Name"), + "is_default": policy.get("IsDefault"), + "addin_roles": addin_roles, + }) + + return { + "role_assignment_policies": policies, + "total_policies": len(policies), + "default_policy": default_policy, + "policies_allowing_addin_install": policies_allowing_addins, + } diff --git a/engine/collectors/exchange/organization/__init__.py b/engine/collectors/exchange/organization/__init__.py new file mode 100644 index 00000000..aa39221b --- /dev/null +++ b/engine/collectors/exchange/organization/__init__.py @@ -0,0 +1 @@ +"""Exchange Online Organization collectors.""" diff --git a/engine/collectors/exchange/organization/organization_config.py b/engine/collectors/exchange/organization/organization_config.py new file mode 100644 index 00000000..1b18d0ed --- /dev/null +++ b/engine/collectors/exchange/organization/organization_config.py @@ -0,0 +1,52 @@ +"""Organization config collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.3.6, 1.3.9, 6.1.1, 6.5.1, 6.5.2, 6.5.5 + +Control Descriptions: + 1.3.6 - Ensure Customer Lockbox is enabled + 1.3.9 - Ensure OAuth authentication is enabled + 6.1.1 - Ensure AuditDisabled is set to False (mailbox auditing at org level) + 6.5.1 - Ensure SMTP AUTH is disabled + 6.5.2 - Ensure all senders are notified when mail is blocked + 6.5.5 - Ensure Direct Send submissions are rejected + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-OrganizationConfig +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class OrganizationConfigDataCollector(BasePowerShellCollector): + """Collects organization config for CIS compliance evaluation. + + This collector retrieves Exchange organization configuration including + customer lockbox, OAuth, audit settings, SMTP AUTH, and Direct Send settings. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect organization configuration data. + + Returns: + Dict containing: + - organization_config: Full organization configuration + - customer_lockbox_enabled: Customer lockbox status (CIS 1.3.6) + - oauth_enabled: OAuth authentication status + - audit_disabled: Whether audit is disabled (CIS 6.1.1) + - reject_direct_send: Whether direct send is rejected (CIS 6.5.5) + """ + config = await client.run_cmdlet("ExchangeOnline", "Get-OrganizationConfig") + + return { + "organization_config": config, + "customer_lockbox_enabled": config.get("CustomerLockBoxEnabled"), + "oauth_enabled": config.get("OAuth2ClientProfileEnabled"), + "audit_disabled": config.get("AuditDisabled"), + "reject_direct_send": config.get("RejectDirectSend"), + } diff --git a/engine/collectors/exchange/organization/owa_mailbox_policy.py b/engine/collectors/exchange/organization/owa_mailbox_policy.py new file mode 100644 index 00000000..43aebed7 --- /dev/null +++ b/engine/collectors/exchange/organization/owa_mailbox_policy.py @@ -0,0 +1,66 @@ +"""OWA mailbox policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.3.9, 6.3.1, 6.5.3 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-OwaMailboxPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class OwaMailboxPolicyDataCollector(BasePowerShellCollector): + """Collects OWA mailbox policy settings for CIS compliance evaluation. + + This collector retrieves OWA settings including bookings, add-ins, + and storage provider configurations. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect OWA mailbox policy data. + + Returns: + Dict containing: + - owa_policies: List of OWA mailbox policies + - policies_with_external_storage: Policies allowing external storage + - policies_with_bookings: Policies with Bookings enabled + """ + policies = await client.run_cmdlet("ExchangeOnline", "Get-OwaMailboxPolicy") + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Find default policy + default_policy = next( + (p for p in policies if p.get("IsDefault")), + policies[0] if policies else None + ) + + # Check for policies with external storage enabled + policies_with_external_storage = [ + p.get("Name") for p in policies + if p.get("AdditionalStorageProvidersAvailable") + ] + + # Check for policies with Bookings enabled + policies_with_bookings = [ + p.get("Name") for p in policies + if p.get("BookingsMailboxCreationEnabled") + ] + + return { + "owa_policies": policies, + "total_policies": len(policies), + "default_policy": default_policy, + "policies_with_external_storage": policies_with_external_storage, + "policies_with_bookings": policies_with_bookings, + } diff --git a/engine/collectors/exchange/organization/sharing_policy.py b/engine/collectors/exchange/organization/sharing_policy.py new file mode 100644 index 00000000..202a89d5 --- /dev/null +++ b/engine/collectors/exchange/organization/sharing_policy.py @@ -0,0 +1,65 @@ +"""Sharing policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 1.3.3 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-SharingPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class SharingPolicyDataCollector(BasePowerShellCollector): + """Collects sharing policy settings for CIS compliance evaluation. + + This collector retrieves external calendar sharing settings + to verify proper sharing restrictions are configured. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect sharing policy data. + + Returns: + Dict containing: + - sharing_policies: List of sharing policies + - default_policy: The default sharing policy + - policies_allowing_external: Policies that allow external sharing + """ + policies = await client.run_cmdlet("ExchangeOnline", "Get-SharingPolicy") + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Find default policy + default_policy = next( + (p for p in policies if p.get("Default")), + policies[0] if policies else None + ) + + # Check for policies allowing external sharing + # Domains field contains sharing rules - if it has entries, sharing is enabled + policies_allowing_external = [] + for policy in policies: + domains = policy.get("Domains", []) + if domains: + policies_allowing_external.append({ + "name": policy.get("Name"), + "domains": domains, + "enabled": policy.get("Enabled"), + }) + + return { + "sharing_policies": policies, + "total_policies": len(policies), + "default_policy": default_policy, + "policies_allowing_external": policies_allowing_external, + } diff --git a/engine/collectors/exchange/organization/transport_config.py b/engine/collectors/exchange/organization/transport_config.py new file mode 100644 index 00000000..956a5160 --- /dev/null +++ b/engine/collectors/exchange/organization/transport_config.py @@ -0,0 +1,38 @@ +"""Transport config collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 6.5.4 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-TransportConfig +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class TransportConfigDataCollector(BasePowerShellCollector): + """Collects transport config for CIS compliance evaluation. + + This collector retrieves SMTP client authentication settings + at the organization level. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect transport configuration data. + + Returns: + Dict containing: + - transport_config: Full transport configuration + - smtp_client_authentication_disabled: SMTP client auth status (CIS 6.5.4) + """ + config = await client.run_cmdlet("ExchangeOnline", "Get-TransportConfig") + + return { + "transport_config": config, + "smtp_client_authentication_disabled": config.get("SmtpClientAuthenticationDisabled"), + } diff --git a/engine/collectors/exchange/protection/__init__.py b/engine/collectors/exchange/protection/__init__.py new file mode 100644 index 00000000..ccb539c6 --- /dev/null +++ b/engine/collectors/exchange/protection/__init__.py @@ -0,0 +1 @@ +"""Exchange Online Protection collectors.""" diff --git a/engine/collectors/exchange/protection/anti_phish_policy.py b/engine/collectors/exchange/protection/anti_phish_policy.py new file mode 100644 index 00000000..64e759bf --- /dev/null +++ b/engine/collectors/exchange/protection/anti_phish_policy.py @@ -0,0 +1,51 @@ +"""Anti-phish policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.7 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-AntiPhishPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class AntiPhishPolicyDataCollector(BasePowerShellCollector): + """Collects anti-phishing policy for CIS compliance evaluation. + + This collector retrieves anti-phishing policy configuration + to verify proper phishing protection is enabled. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect anti-phish policy data. + + Returns: + Dict containing: + - anti_phish_policies: List of anti-phishing policies + - default_policy: The default policy (Office365 AntiPhish Default) + """ + policies = await client.run_cmdlet("ExchangeOnline", "Get-AntiPhishPolicy") + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Find default policy + default_policy = next( + (p for p in policies if p.get("IsDefault")), + None + ) + + return { + "anti_phish_policies": policies, + "total_policies": len(policies), + "default_policy": default_policy, + } diff --git a/engine/collectors/exchange/protection/atp_policy_o365.py b/engine/collectors/exchange/protection/atp_policy_o365.py new file mode 100644 index 00000000..06187e57 --- /dev/null +++ b/engine/collectors/exchange/protection/atp_policy_o365.py @@ -0,0 +1,42 @@ +"""ATP policy for Office 365 collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.5 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-AtpPolicyForO365 +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class AtpPolicyO365DataCollector(BasePowerShellCollector): + """Collects ATP policy for O365 for CIS compliance evaluation. + + This collector retrieves Safe Attachments settings for SharePoint, + OneDrive, and Teams to verify protection is enabled. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect ATP policy for O365 data. + + Returns: + Dict containing: + - atp_policy: The ATP policy for Office 365 + - enable_atp_for_spo_teams_odb: Safe Docs for SPO/Teams/OneDrive status + - enable_safe_docs: Safe Documents status + - allow_safe_docs_open: Allow Safe Docs open in Protected View + """ + policy = await client.run_cmdlet("ExchangeOnline", "Get-AtpPolicyForO365") + + return { + "atp_policy": policy, + "enable_atp_for_spo_teams_odb": policy.get("EnableATPForSPOTeamsODB"), + "enable_safe_docs": policy.get("EnableSafeDocs"), + "allow_safe_docs_open": policy.get("AllowSafeDocsOpen"), + } diff --git a/engine/collectors/exchange/protection/hosted_connection_filter.py b/engine/collectors/exchange/protection/hosted_connection_filter.py new file mode 100644 index 00000000..cf71ab03 --- /dev/null +++ b/engine/collectors/exchange/protection/hosted_connection_filter.py @@ -0,0 +1,56 @@ +"""Hosted connection filter collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.12, 2.1.13 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-HostedConnectionFilterPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class HostedConnectionFilterDataCollector(BasePowerShellCollector): + """Collects connection filter policy for CIS compliance evaluation. + + This collector retrieves IP allow list and safe list settings + to verify connection filtering is properly configured. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect hosted connection filter data. + + Returns: + Dict containing: + - connection_filter_policies: List of connection filter policies + - ip_allow_list: IP addresses in allow list + - enable_safe_list: Safe list status + """ + policies = await client.run_cmdlet( + "ExchangeOnline", "Get-HostedConnectionFilterPolicy" + ) + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Get default policy settings + default_policy = next( + (p for p in policies if p.get("IsDefault")), + policies[0] if policies else None + ) + + return { + "connection_filter_policies": policies, + "total_policies": len(policies), + "default_policy": default_policy, + "ip_allow_list": default_policy.get("IPAllowList", []) if default_policy else [], + "enable_safe_list": default_policy.get("EnableSafeList") if default_policy else None, + } diff --git a/engine/collectors/exchange/protection/hosted_content_filter.py b/engine/collectors/exchange/protection/hosted_content_filter.py new file mode 100644 index 00000000..59c53133 --- /dev/null +++ b/engine/collectors/exchange/protection/hosted_content_filter.py @@ -0,0 +1,56 @@ +"""Hosted content filter collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.14 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-HostedContentFilterPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class HostedContentFilterDataCollector(BasePowerShellCollector): + """Collects content filter policy for CIS compliance evaluation. + + This collector retrieves allowed sender domains configuration + to verify content filtering is properly configured. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect hosted content filter data. + + Returns: + Dict containing: + - content_filter_policies: List of content filter policies + - allowed_sender_domains: Domains allowed to bypass filtering + - allowed_senders: Senders allowed to bypass filtering + """ + policies = await client.run_cmdlet( + "ExchangeOnline", "Get-HostedContentFilterPolicy" + ) + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Get default policy settings + default_policy = next( + (p for p in policies if p.get("IsDefault")), + policies[0] if policies else None + ) + + return { + "content_filter_policies": policies, + "total_policies": len(policies), + "default_policy": default_policy, + "allowed_sender_domains": default_policy.get("AllowedSenderDomains", []) if default_policy else [], + "allowed_senders": default_policy.get("AllowedSenders", []) if default_policy else [], + } diff --git a/engine/collectors/exchange/protection/hosted_outbound_spam_filter.py b/engine/collectors/exchange/protection/hosted_outbound_spam_filter.py new file mode 100644 index 00000000..696a60a1 --- /dev/null +++ b/engine/collectors/exchange/protection/hosted_outbound_spam_filter.py @@ -0,0 +1,57 @@ +"""Hosted outbound spam filter collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.6, 2.1.15, 6.2.1 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-HostedOutboundSpamFilterPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class HostedOutboundSpamFilterDataCollector(BasePowerShellCollector): + """Collects outbound spam filter policy for CIS compliance evaluation. + + This collector retrieves outbound spam notifications, message limits, + and auto-forwarding settings for compliance evaluation. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect hosted outbound spam filter data. + + Returns: + Dict containing: + - outbound_spam_policies: List of outbound spam filter policies + - default_policy: The default policy + - auto_forwarding_mode: Auto-forwarding configuration + """ + policies = await client.run_cmdlet( + "ExchangeOnline", "Get-HostedOutboundSpamFilterPolicy" + ) + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Get default policy + default_policy = next( + (p for p in policies if p.get("IsDefault")), + policies[0] if policies else None + ) + + return { + "outbound_spam_policies": policies, + "total_policies": len(policies), + "default_policy": default_policy, + "auto_forwarding_mode": default_policy.get("AutoForwardingMode") if default_policy else None, + "bcc_suspicious_outbound_mail": default_policy.get("BccSuspiciousOutboundMail") if default_policy else None, + "notify_outbound_spam": default_policy.get("NotifyOutboundSpam") if default_policy else None, + } diff --git a/engine/collectors/exchange/protection/malware_filter_policy.py b/engine/collectors/exchange/protection/malware_filter_policy.py new file mode 100644 index 00000000..18732eb9 --- /dev/null +++ b/engine/collectors/exchange/protection/malware_filter_policy.py @@ -0,0 +1,55 @@ +"""Malware filter policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.2, 2.1.3, 2.1.11 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-MalwareFilterPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class MalwareFilterPolicyDataCollector(BasePowerShellCollector): + """Collects malware filter policy for CIS compliance evaluation. + + This collector retrieves anti-malware settings including common + attachment filter, admin notifications, and comprehensive filtering. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect malware filter policy data. + + Returns: + Dict containing: + - malware_filter_policies: List of malware filter policies + - default_policy: The default policy + - enable_file_filter: Common attachment types filter status + """ + policies = await client.run_cmdlet("ExchangeOnline", "Get-MalwareFilterPolicy") + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Get default policy + default_policy = next( + (p for p in policies if p.get("IsDefault")), + policies[0] if policies else None + ) + + return { + "malware_filter_policies": policies, + "total_policies": len(policies), + "default_policy": default_policy, + "enable_file_filter": default_policy.get("EnableFileFilter") if default_policy else None, + "file_types": default_policy.get("FileTypes", []) if default_policy else [], + "zap_enabled": default_policy.get("ZapEnabled") if default_policy else None, + } diff --git a/engine/collectors/exchange/protection/safe_attachment_policy.py b/engine/collectors/exchange/protection/safe_attachment_policy.py new file mode 100644 index 00000000..d2985324 --- /dev/null +++ b/engine/collectors/exchange/protection/safe_attachment_policy.py @@ -0,0 +1,57 @@ +"""Safe Attachment policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.4 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-SafeAttachmentPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class SafeAttachmentPolicyDataCollector(BasePowerShellCollector): + """Collects Safe Attachments policy for CIS compliance evaluation. + + This collector retrieves Safe Attachments configuration to verify + attachment scanning protection is properly enabled. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect Safe Attachment policy data. + + Returns: + Dict containing: + - safe_attachment_policies: List of Safe Attachment policies + - policies_with_protection: Policies with protection enabled + """ + policies = await client.run_cmdlet("ExchangeOnline", "Get-SafeAttachmentPolicy") + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Find policies with protection enabled (Action != "Off") + policies_with_protection = [ + { + "name": p.get("Name"), + "action": p.get("Action"), + "enable": p.get("Enable"), + "redirect": p.get("Redirect"), + } + for p in policies + if p.get("Enable") and p.get("Action") != "Off" + ] + + return { + "safe_attachment_policies": policies, + "total_policies": len(policies), + "policies_with_protection": policies_with_protection, + } diff --git a/engine/collectors/exchange/protection/safe_links_policy.py b/engine/collectors/exchange/protection/safe_links_policy.py new file mode 100644 index 00000000..baa5b60e --- /dev/null +++ b/engine/collectors/exchange/protection/safe_links_policy.py @@ -0,0 +1,59 @@ +"""Safe Links policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.1.1 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-SafeLinksPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class SafeLinksPolicyDataCollector(BasePowerShellCollector): + """Collects Safe Links policy for CIS compliance evaluation. + + This collector retrieves Safe Links configuration for Office + applications to verify URL protection is properly enabled. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect Safe Links policy data. + + Returns: + Dict containing: + - safe_links_policies: List of Safe Links policies + - policies_with_protection: Policies with URL protection enabled + """ + policies = await client.run_cmdlet("ExchangeOnline", "Get-SafeLinksPolicy") + + # Handle None, single policy, or list + if policies is None: + policies = [] + elif isinstance(policies, dict): + policies = [policies] + + # Find policies with protection enabled + policies_with_protection = [ + { + "name": p.get("Name"), + "enable_safe_links_for_email": p.get("EnableSafeLinksForEmail"), + "enable_safe_links_for_office": p.get("EnableSafeLinksForOffice"), + "enable_safe_links_for_teams": p.get("EnableSafeLinksForTeams"), + "scan_urls": p.get("ScanUrls"), + "track_clicks": p.get("TrackClicks"), + } + for p in policies + if p.get("EnableSafeLinksForEmail") or p.get("EnableSafeLinksForOffice") + ] + + return { + "safe_links_policies": policies, + "total_policies": len(policies), + "policies_with_protection": policies_with_protection, + } diff --git a/engine/collectors/exchange/protection/teams_protection_policy.py b/engine/collectors/exchange/protection/teams_protection_policy.py new file mode 100644 index 00000000..dfc2da09 --- /dev/null +++ b/engine/collectors/exchange/protection/teams_protection_policy.py @@ -0,0 +1,39 @@ +"""Teams protection policy collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 2.4.4 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-TeamsProtectionPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class TeamsProtectionPolicyDataCollector(BasePowerShellCollector): + """Collects Teams protection policy for CIS compliance evaluation. + + This collector retrieves zero-hour auto purge settings for Teams + to verify protection against malicious content. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect Teams protection policy data. + + Returns: + Dict containing: + - teams_protection_policy: The Teams protection policy + - zap_enabled: Zero-hour auto purge status for Teams + """ + policy = await client.run_cmdlet("ExchangeOnline", "Get-TeamsProtectionPolicy") + + return { + "teams_protection_policy": policy, + "zap_enabled": policy.get("ZapEnabled") if policy else None, + "malware_scan_enabled": policy.get("MalwareScanEnabled") if policy else None, + } diff --git a/engine/collectors/exchange/transport/__init__.py b/engine/collectors/exchange/transport/__init__.py new file mode 100644 index 00000000..782d8547 --- /dev/null +++ b/engine/collectors/exchange/transport/__init__.py @@ -0,0 +1 @@ +"""Exchange Online Transport collectors.""" diff --git a/engine/collectors/exchange/transport/external_in_outlook.py b/engine/collectors/exchange/transport/external_in_outlook.py new file mode 100644 index 00000000..21a2ed51 --- /dev/null +++ b/engine/collectors/exchange/transport/external_in_outlook.py @@ -0,0 +1,40 @@ +"""External in Outlook collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 6.2.3 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-ExternalInOutlook +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class ExternalInOutlookDataCollector(BasePowerShellCollector): + """Collects external sender tagging settings for CIS compliance evaluation. + + This collector retrieves external sender identification settings + to verify external senders are properly tagged in Outlook. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect external in Outlook data. + + Returns: + Dict containing: + - external_in_outlook_settings: External sender tagging config + - enabled: Whether external tagging is enabled + - allowed_senders: Senders exempt from tagging + """ + settings = await client.run_cmdlet("ExchangeOnline", "Get-ExternalInOutlook") + + return { + "external_in_outlook_settings": settings, + "enabled": settings.get("Enabled") if settings else None, + "allowed_senders": settings.get("AllowList", []) if settings else [], + } diff --git a/engine/collectors/exchange/transport/transport_rules.py b/engine/collectors/exchange/transport/transport_rules.py new file mode 100644 index 00000000..322293be --- /dev/null +++ b/engine/collectors/exchange/transport/transport_rules.py @@ -0,0 +1,111 @@ +"""Transport rules collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 6.2.1, 6.2.2 + +Connection Method: Exchange Online PowerShell (via Docker container) +Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter +Required Cmdlets: Get-TransportRule, Get-HostedOutboundSpamFilterPolicy +Required Permissions: Exchange.ManageAsApp + Exchange role assignment +""" + +from typing import Any + +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient + + +class TransportRulesDataCollector(BasePowerShellCollector): + """Collects transport rules for CIS compliance evaluation. + + This collector retrieves mail transport rules to check for + mail forwarding rules and domain whitelisting configurations. + Also retrieves outbound spam filter policies for auto-forwarding settings. + """ + + async def collect(self, client: PowerShellClient) -> dict[str, Any]: + """Collect transport rules and outbound spam filter policy data. + + Returns: + Dict containing: + - transport_rules: List of transport rules + - forwarding_rules: Rules that forward mail externally + - whitelist_rules: Rules that whitelist domains + - outbound_spam_filter_policies: List of outbound spam filter policies + - auto_forwarding_blocked: Whether auto-forwarding is blocked in all policies + """ + rules = await client.run_cmdlet("ExchangeOnline", "Get-TransportRule") + + # Handle None, single rule, or list + if rules is None: + rules = [] + elif isinstance(rules, dict): + rules = [rules] + + # Find rules that forward mail + forwarding_rules = [ + { + "name": r.get("Name"), + "state": r.get("State"), + "redirect_to": r.get("RedirectMessageTo"), + "forward_to": r.get("BlindCopyTo"), + } + for r in rules + if r.get("RedirectMessageTo") or r.get("BlindCopyTo") + ] + + # Find rules that whitelist domains (SetSCL -1 AND SenderDomainIs set) + # Per CIS 6.2.2: rule is non-compliant if it has BOTH properties together + whitelist_rules = [] + for r in rules: + # Convert SetSCL to int (may come as string from JSON) + set_scl_raw = r.get("SetSCL") + try: + set_scl = int(set_scl_raw) if set_scl_raw is not None else None + except (ValueError, TypeError): + set_scl = None + + sender_domain = r.get("SenderDomainIs") + + # Rule is a whitelist rule if it has BOTH SetSCL = -1 AND SenderDomainIs + if set_scl == -1 and sender_domain: + whitelist_rules.append({ + "name": r.get("Name"), + "state": r.get("State"), + "sender_domain": sender_domain, + "set_scl": set_scl, + }) + + # Get outbound spam filter policies for auto-forwarding check (CIS 6.2.1) + spam_policies = await client.run_cmdlet( + "ExchangeOnline", "Get-HostedOutboundSpamFilterPolicy" + ) + + # Handle None, single policy, or list + if spam_policies is None: + spam_policies = [] + elif isinstance(spam_policies, dict): + spam_policies = [spam_policies] + + # Extract auto-forwarding mode from each policy + outbound_policies = [ + { + "name": p.get("Name"), + "auto_forwarding_mode": p.get("AutoForwardingMode"), + } + for p in spam_policies + ] + + # Check if all policies have AutoForwardingMode set to "Off" + auto_forwarding_blocked = all( + p.get("AutoForwardingMode") == "Off" for p in spam_policies + ) if spam_policies else False + + return { + "transport_rules": rules, + "total_rules": len(rules), + "forwarding_rules": forwarding_rules, + "whitelist_rules": whitelist_rules, + "outbound_spam_filter_policies": outbound_policies, + "auto_forwarding_blocked": auto_forwarding_blocked, + } diff --git a/engine/collectors/fabric_client.py b/engine/collectors/fabric_client.py new file mode 100644 index 00000000..5e8b97de --- /dev/null +++ b/engine/collectors/fabric_client.py @@ -0,0 +1,115 @@ +"""Microsoft Fabric Admin API client. + +The Fabric API is separate from the Microsoft Graph API and requires +different authentication scopes and base URL. + +API Documentation: https://learn.microsoft.com/en-us/rest/api/fabric/admin +""" + +from typing import Any + +import httpx +from msal import ConfidentialClientApplication + + +class FabricClient: + """Client for Microsoft Fabric Admin API.""" + + FABRIC_BASE_URL = "https://api.fabric.microsoft.com/v1" + FABRIC_SCOPE = "https://api.fabric.microsoft.com/.default" + + def __init__(self, tenant_id: str, client_id: str, client_secret: str): + """Initialize the Fabric API client. + + Args: + tenant_id: Azure AD tenant ID + client_id: App registration client ID + client_secret: App registration client secret + """ + self.tenant_id = tenant_id + self.client_id = client_id + self.client_secret = client_secret + self._access_token: str | None = None + + # Initialize MSAL client + self._msal_app = ConfidentialClientApplication( + client_id=client_id, + client_credential=client_secret, + authority=f"https://login.microsoftonline.com/{tenant_id}", + ) + + async def _get_access_token(self) -> str: + """Get or refresh the Fabric API access token.""" + if self._access_token: + return self._access_token + + # Acquire token for Fabric API + result = self._msal_app.acquire_token_for_client(scopes=[self.FABRIC_SCOPE]) + + if "access_token" not in result: + error = result.get("error_description", result.get("error", "Unknown error")) + raise Exception(f"Failed to acquire Fabric token: {error}") + + self._access_token = result["access_token"] + return self._access_token + + async def _request( + self, + method: str, + endpoint: str, + params: dict | None = None, + json_data: dict | None = None, + ) -> dict[str, Any]: + """Make a request to the Fabric Admin API. + + Args: + method: HTTP method (GET, POST, etc.) + endpoint: API endpoint path (e.g., /admin/tenantsettings) + params: Query parameters + json_data: JSON body for POST/PATCH requests + + Returns: + Response JSON as dictionary + """ + token = await self._get_access_token() + + async with httpx.AsyncClient() as client: + response = await client.request( + method=method, + url=f"{self.FABRIC_BASE_URL}{endpoint}", + headers={"Authorization": f"Bearer {token}"}, + params=params, + json=json_data, + timeout=60.0, + ) + response.raise_for_status() + return response.json() if response.content else {} + + async def get( + self, endpoint: str, params: dict | None = None + ) -> dict[str, Any]: + """GET request to Fabric Admin API. + + Args: + endpoint: API endpoint path + params: Query parameters + + Returns: + Response JSON + """ + return await self._request("GET", endpoint, params=params) + + async def get_tenant_settings(self) -> list[dict[str, Any]]: + """Get all Fabric tenant settings. + + Returns: + List of tenant setting objects with structure: + - settingName: Internal name of the setting + - title: Display name + - enabled: Whether the setting is enabled + - tenantSettingGroup: Category grouping + - canSpecifySecurityGroups: Whether security groups can be specified + - enabledSecurityGroups: List of enabled security group IDs + """ + response = await self.get("/admin/tenantsettings") + return response.get("tenantSettings", []) diff --git a/engine/collectors/graph_client.py b/engine/collectors/graph_client.py new file mode 100644 index 00000000..349c80c2 --- /dev/null +++ b/engine/collectors/graph_client.py @@ -0,0 +1,132 @@ +"""Microsoft Graph API client.""" + +from typing import Any + +import httpx +from msal import ConfidentialClientApplication + + +class GraphClient: + """Client for Microsoft Graph API.""" + + GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0" + GRAPH_BETA_URL = "https://graph.microsoft.com/beta" + + def __init__(self, tenant_id: str, client_id: str, client_secret: str): + self.tenant_id = tenant_id + self.client_id = client_id + self.client_secret = client_secret + self._access_token: str | None = None + + # Initialize MSAL client + self._msal_app = ConfidentialClientApplication( + client_id=client_id, + client_credential=client_secret, + authority=f"https://login.microsoftonline.com/{tenant_id}", + ) + + async def _get_access_token(self) -> str: + """Get or refresh the access token.""" + if self._access_token: + return self._access_token + + # Acquire token for Graph API + result = self._msal_app.acquire_token_for_client( + scopes=["https://graph.microsoft.com/.default"] + ) + + if "access_token" not in result: + error = result.get("error_description", result.get("error", "Unknown error")) + raise Exception(f"Failed to acquire token: {error}") + + self._access_token = result["access_token"] + return self._access_token + + async def _request( + self, + method: str, + endpoint: str, + beta: bool = False, + params: dict | None = None, + json_data: dict | None = None, + ) -> dict[str, Any]: + """Make a request to the Graph API.""" + token = await self._get_access_token() + base_url = self.GRAPH_BETA_URL if beta else self.GRAPH_BASE_URL + + async with httpx.AsyncClient() as client: + response = await client.request( + method=method, + url=f"{base_url}{endpoint}", + headers={"Authorization": f"Bearer {token}"}, + params=params, + json=json_data, + timeout=60.0, + ) + response.raise_for_status() + return response.json() if response.content else {} + + async def get( + self, endpoint: str, beta: bool = False, params: dict | None = None + ) -> dict[str, Any]: + """GET request to Graph API.""" + return await self._request("GET", endpoint, beta=beta, params=params) + + async def get_all_pages( + self, + endpoint: str, + beta: bool = False, + params: dict | None = None, + max_pages: int = 100, + ) -> list[dict[str, Any]]: + """Get all pages of a paginated endpoint.""" + all_items: list[dict[str, Any]] = [] + current_endpoint = endpoint + current_params = params + + for _ in range(max_pages): + response = await self.get(current_endpoint, beta=beta, params=current_params) + items = response.get("value", []) + all_items.extend(items) + + # Check for next page + next_link = response.get("@odata.nextLink") + if not next_link: + break + + # Parse next link - it's a full URL + base_url = self.GRAPH_BETA_URL if beta else self.GRAPH_BASE_URL + current_endpoint = next_link.replace(base_url, "") + current_params = None # Params are in the URL + + return all_items + + async def get_users(self) -> list[dict[str, Any]]: + """Get all users.""" + return await self.get_all_pages( + "/users", + params={"$select": "id,userPrincipalName,displayName,accountEnabled,userType"}, + ) + + async def get_directory_roles(self) -> list[dict[str, Any]]: + """Get all directory roles.""" + return await self.get_all_pages("/directoryRoles") + + async def get_role_members(self, role_id: str) -> list[dict[str, Any]]: + """Get members of a directory role.""" + return await self.get_all_pages(f"/directoryRoles/{role_id}/members") + + async def get_conditional_access_policies(self) -> list[dict[str, Any]]: + """Get all Conditional Access policies.""" + return await self.get_all_pages("/identity/conditionalAccess/policies") + + async def get_authentication_methods(self, user_id: str) -> list[dict[str, Any]]: + """Get authentication methods for a user.""" + response = await self.get( + f"/users/{user_id}/authentication/methods", beta=True + ) + return response.get("value", []) + + async def get_domains(self) -> list[dict[str, Any]]: + """Get all domains.""" + return await self.get_all_pages("/domains") diff --git a/engine/collectors/m365/__init__.py b/engine/collectors/m365/__init__.py new file mode 100644 index 00000000..4268ff25 --- /dev/null +++ b/engine/collectors/m365/__init__.py @@ -0,0 +1 @@ +"""Microsoft 365 service collectors.""" diff --git a/engine/collectors/powershell_base.py b/engine/collectors/powershell_base.py new file mode 100644 index 00000000..8503b5aa --- /dev/null +++ b/engine/collectors/powershell_base.py @@ -0,0 +1,30 @@ +"""Base class for PowerShell-based collectors.""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collectors.powershell_client import PowerShellClient + + +class BasePowerShellCollector(ABC): + """Abstract base class for PowerShell collectors. + + This base class is used for collectors that require PowerShell cmdlets + from Exchange Online, Microsoft Teams, or Security & Compliance modules. + + Authentication uses client secret via MSAL to obtain access tokens, + which are then passed to PowerShell cmdlets via the -AccessToken parameter. + """ + + @abstractmethod + async def collect(self, client: "PowerShellClient") -> dict[str, Any]: + """Collect data using PowerShell cmdlets. + + Args: + client: The PowerShell client to use for data collection. + + Returns: + Dictionary of collected data to be passed to OPA for evaluation. + """ + pass diff --git a/engine/collectors/powershell_client.py b/engine/collectors/powershell_client.py new file mode 100644 index 00000000..cff9f7ed --- /dev/null +++ b/engine/collectors/powershell_client.py @@ -0,0 +1,353 @@ +"""PowerShell client for Exchange Online, Teams, and Security & Compliance. + +This module provides connectivity to Microsoft 365 PowerShell modules using +client secret authentication via MSAL access tokens. PowerShell execution +can happen either: +- Inside a Docker container (local development with Docker Desktop) +- Via HTTP to a PowerShell service (Docker Compose / production) + +Supported modules: +- ExchangeOnlineManagement (Exchange Online via -AccessToken) +- ExchangeOnlineManagement (IPPSSession via -AccessToken) +- MicrosoftTeams (via -AccessTokens) + +Authentication Flow: +1. Use MSAL ConfidentialClientApplication with client_id + client_secret +2. Acquire token for the appropriate scope +3. Pass token to Docker container (via env var) or HTTP service (via request body) +4. Container/service runs PowerShell cmdlet and returns JSON +""" + +import json +import subprocess +from pathlib import Path +from typing import Any + +import httpx +from msal import ConfidentialClientApplication + + +class PowerShellExecutionError(Exception): + """Raised when PowerShell execution fails.""" + + pass + + +class PowerShellClient: + """Client for PowerShell-based M365 connections using Docker or HTTP service.""" + + # Service-specific scopes for token acquisition + EXCHANGE_SCOPE = "https://outlook.office365.com/.default" + TEAMS_SCOPE = "https://api.interfaces.records.teams.microsoft.com/.default" + COMPLIANCE_SCOPE = "https://ps.compliance.protection.outlook.com/.default" + + DOCKER_IMAGE = "autoaudit-powershell" + + def __init__( + self, + tenant_id: str, + client_id: str, + client_secret: str, + service_url: str | None = None, + ): + """Initialize PowerShell client. + + Args: + tenant_id: Azure AD tenant ID + client_id: Application (client) ID + client_secret: Client secret for authentication + service_url: Optional URL of PowerShell HTTP service (e.g., http://powershell-service:8001). + If provided, uses HTTP instead of spawning Docker containers. + """ + self.tenant_id = tenant_id + self.client_id = client_id + self.client_secret = client_secret + self.service_url = service_url + self._msal_app = ConfidentialClientApplication( + client_id=client_id, + client_credential=client_secret, + authority=f"https://login.microsoftonline.com/{tenant_id}", + ) + self._image_checked = False + + def _ensure_docker_image(self) -> None: + """Build Docker image if it doesn't exist.""" + if self._image_checked: + return + + # Check if Docker is available + try: + subprocess.run( + ["docker", "--version"], + capture_output=True, + check=True, + ) + except FileNotFoundError: + raise PowerShellExecutionError( + "Docker is not installed or not in PATH.\n" + "Install Docker from: https://docs.docker.com/get-docker/" + ) + except subprocess.CalledProcessError as e: + raise PowerShellExecutionError(f"Docker check failed: {e.stderr}") + + # Check if image exists + result = subprocess.run( + ["docker", "images", "-q", self.DOCKER_IMAGE], + capture_output=True, + text=True, + ) + if not result.stdout.strip(): + print(f"Building {self.DOCKER_IMAGE} Docker image (this may take a few minutes)...") + dockerfile_dir = Path(__file__).parent.parent / "docker" / "powershell" + build_result = subprocess.run( + ["docker", "build", "-t", self.DOCKER_IMAGE, str(dockerfile_dir)], + capture_output=True, + text=True, + ) + if build_result.returncode != 0: + raise PowerShellExecutionError( + f"Failed to build Docker image:\n{build_result.stderr}" + ) + print(f"Docker image {self.DOCKER_IMAGE} built successfully.") + + self._image_checked = True + + async def run_cmdlet(self, module: str, cmdlet: str, **params: Any) -> dict[str, Any]: + """Execute a PowerShell cmdlet. + + Uses HTTP service if service_url is configured, otherwise spawns Docker container. + + Args: + module: The PowerShell module (ExchangeOnline, Teams, Compliance) + cmdlet: The cmdlet to run (e.g., Get-OrganizationConfig) + **params: Parameters to pass to the cmdlet + + Returns: + Dict containing cmdlet output. + """ + if self.service_url: + return await self._run_via_service(module, cmdlet, params) + else: + return await self._run_via_docker(module, cmdlet, params) + + async def _run_via_service( + self, module: str, cmdlet: str, params: dict[str, Any] + ) -> dict[str, Any]: + """Execute cmdlet via HTTP service. + + Args: + module: The PowerShell module + cmdlet: The cmdlet to run + params: Parameters for the cmdlet + + Returns: + Dict containing cmdlet output. + """ + # Acquire tokens + graph_token = None + if module == "Teams": + # Teams needs both Graph and Teams tokens + graph_result = self._msal_app.acquire_token_for_client( + scopes=["https://graph.microsoft.com/.default"] + ) + if "access_token" not in graph_result: + error_desc = graph_result.get("error_description", str(graph_result)) + raise RuntimeError(f"Graph token acquisition failed: {error_desc}") + graph_token = graph_result["access_token"] + + teams_result = self._msal_app.acquire_token_for_client( + scopes=[self.TEAMS_SCOPE] + ) + if "access_token" not in teams_result: + error_desc = teams_result.get("error_description", str(teams_result)) + raise RuntimeError(f"Teams token acquisition failed: {error_desc}") + token = teams_result["access_token"] + else: + # Exchange and Compliance use single token + scope = self._get_scope_for_module(module) + result = self._msal_app.acquire_token_for_client(scopes=[scope]) + if "access_token" not in result: + error_desc = result.get("error_description", str(result)) + raise RuntimeError(f"Token acquisition failed: {error_desc}") + token = result["access_token"] + + # Build request payload + payload = { + "module": module, + "cmdlet": cmdlet, + "params": params, + "tenant_id": self.tenant_id, + "token": token, + "graph_token": graph_token, + } + + # Call HTTP service + async with httpx.AsyncClient(timeout=120.0) as client: + response = await client.post( + f"{self.service_url}/execute", + json=payload, + ) + response.raise_for_status() + + result = response.json() + if not result.get("success"): + raise PowerShellExecutionError(result.get("error", "Unknown error")) + + return result.get("data") + + async def _run_via_docker( + self, module: str, cmdlet: str, params: dict[str, Any] + ) -> dict[str, Any]: + """Execute cmdlet by spawning Docker container. + + Args: + module: The PowerShell module + cmdlet: The cmdlet to run + params: Parameters for the cmdlet + + Returns: + Dict containing cmdlet output. + """ + # Ensure Docker image exists + self._ensure_docker_image() + + # Build environment variables for tokens + env_vars = [] + + if module == "Teams": + # Teams needs both Graph and Teams tokens + graph_result = self._msal_app.acquire_token_for_client( + scopes=["https://graph.microsoft.com/.default"] + ) + if "access_token" not in graph_result: + error_desc = graph_result.get("error_description", str(graph_result)) + raise RuntimeError(f"Graph token acquisition failed: {error_desc}") + + teams_result = self._msal_app.acquire_token_for_client( + scopes=[self.TEAMS_SCOPE] + ) + if "access_token" not in teams_result: + error_desc = teams_result.get("error_description", str(teams_result)) + raise RuntimeError(f"Teams token acquisition failed: {error_desc}") + + env_vars = [ + "-e", f"GRAPH_TOKEN={graph_result['access_token']}", + "-e", f"TEAMS_TOKEN={teams_result['access_token']}", + ] + else: + # Exchange and Compliance use single token + scope = self._get_scope_for_module(module) + result = self._msal_app.acquire_token_for_client(scopes=[scope]) + if "access_token" not in result: + error_desc = result.get("error_description", str(result)) + raise RuntimeError(f"Token acquisition failed: {error_desc}") + env_vars = ["-e", f"EXO_TOKEN={result['access_token']}"] + + # Build PowerShell script + script = self._build_script(module, cmdlet, params) + + # Run in Docker container (pass token via env var for security) + docker_cmd = ["docker", "run", "--rm"] + env_vars + [self.DOCKER_IMAGE, script] + proc = subprocess.run( + docker_cmd, + capture_output=True, + text=True, + timeout=120, + ) + + if proc.returncode != 0: + raise PowerShellExecutionError(f"PowerShell execution failed:\n{proc.stderr}") + + # Parse JSON output + try: + return json.loads(proc.stdout) + except json.JSONDecodeError as e: + raise PowerShellExecutionError( + f"Failed to parse PowerShell output as JSON:\n{proc.stdout}\nError: {e}" + ) + + def _build_script(self, module: str, cmdlet: str, params: dict[str, Any]) -> str: + """Build the PowerShell script to execute. + + Args: + module: The module to import and connect + cmdlet: The cmdlet to run + params: Parameters for the cmdlet + + Returns: + PowerShell script as a string + """ + # Build parameter string for cmdlet + param_str = "" + for key, value in params.items(): + if isinstance(value, bool): + param_str += f" -{key}:${str(value).lower()}" + elif isinstance(value, str): + param_str += f' -{key} "{value}"' + else: + param_str += f" -{key} {value}" + + if module == "ExchangeOnline": + return f''' +Import-Module ExchangeOnlineManagement +Connect-ExchangeOnline -AccessToken $env:EXO_TOKEN -Organization "{self.tenant_id}" -ShowBanner:$false +try {{ + $result = {cmdlet}{param_str} + if ($null -eq $result) {{ + Write-Output 'null' + }} else {{ + $result | ConvertTo-Json -Depth 10 + }} +}} finally {{ + Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue +}} +''' + elif module == "Compliance": + return f''' +Import-Module ExchangeOnlineManagement +Connect-IPPSSession -AccessToken $env:EXO_TOKEN -Organization "{self.tenant_id}" -ShowBanner:$false +try {{ + $result = {cmdlet}{param_str} + if ($null -eq $result) {{ + Write-Output 'null' + }} else {{ + $result | ConvertTo-Json -Depth 10 + }} +}} finally {{ + Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue +}} +''' + elif module == "Teams": + # Teams module uses -AccessTokens (plural) with Graph and Teams tokens + return f''' +Import-Module MicrosoftTeams +Connect-MicrosoftTeams -AccessTokens @($env:GRAPH_TOKEN, $env:TEAMS_TOKEN) -TenantId "{self.tenant_id}" +try {{ + $result = {cmdlet}{param_str} + if ($null -eq $result) {{ + Write-Output 'null' + }} else {{ + $result | ConvertTo-Json -Depth 10 + }} +}} finally {{ + Disconnect-MicrosoftTeams -ErrorAction SilentlyContinue +}} +''' + else: + raise ValueError(f"Unsupported module: {module}") + + def _get_scope_for_module(self, module: str) -> str: + """Get the appropriate scope for a PowerShell module. + + Args: + module: The module name (ExchangeOnline, Teams, Compliance) + + Returns: + The OAuth scope for the module. + """ + scopes = { + "ExchangeOnline": self.EXCHANGE_SCOPE, + "Teams": self.TEAMS_SCOPE, + "Compliance": self.COMPLIANCE_SCOPE, + } + return scopes.get(module, self.EXCHANGE_SCOPE) diff --git a/engine/collectors/registry.py b/engine/collectors/registry.py new file mode 100644 index 00000000..4959514e --- /dev/null +++ b/engine/collectors/registry.py @@ -0,0 +1,231 @@ +"""Registry of available data collectors.""" + +from collectors.base import BaseDataCollector + +# Applications +from collectors.entra.applications.apps_and_services_settings import ( + AppsAndServicesSettingsDataCollector, +) +from collectors.entra.applications.forms_settings import FormsSettingsDataCollector +from collectors.entra.applications.service_principals import ( + ServicePrincipalsDataCollector, +) + +# Authentication +from collectors.entra.authentication.authentication_methods import ( + AuthenticationMethodsDataCollector, +) +from collectors.entra.authentication.mfa_fatigue_protection import ( + MfaFatigueProtectionDataCollector, +) +from collectors.entra.authentication.mfa_registration_report import ( + MFARegistrationReportDataCollector, +) +from collectors.entra.authentication.password_protection import ( + PasswordProtectionDataCollector, +) + +# Conditional Access +from collectors.entra.conditional_access.conditional_access_policies import ( + ConditionalAccessPoliciesDataCollector, +) +from collectors.entra.conditional_access.legacy_auth_block import ( + LegacyAuthBlockDataCollector, +) + +# Devices +from collectors.entra.devices.device_management_settings import ( + DeviceManagementSettingsDataCollector, +) +from collectors.entra.devices.device_registration_policy import ( + DeviceRegistrationPolicyDataCollector, +) +from collectors.entra.devices.enrollment_restrictions import ( + EnrollmentRestrictionsDataCollector, +) + +# Domains +from collectors.entra.domains.domains import DomainsDataCollector +from collectors.entra.domains.password_policy import PasswordPolicyDataCollector + +# Governance +from collectors.entra.governance.access_reviews import AccessReviewsDataCollector +from collectors.entra.governance.pim_role_policies import PimRolePoliciesDataCollector + +# Groups +from collectors.entra.groups.groups import GroupsDataCollector + +# Policies +from collectors.entra.policies.activity_timeout_policy import ( + ActivityTimeoutPolicyDataCollector, +) +from collectors.entra.policies.admin_consent_request_policy import ( + AdminConsentRequestPolicyDataCollector, +) +from collectors.entra.policies.authorization_policy import ( + AuthorizationPolicyDataCollector, +) +from collectors.entra.policies.b2b_policy import B2BPolicyDataCollector + +# Roles +from collectors.entra.roles.cloud_only_admins import CloudOnlyAdminsDataCollector +from collectors.entra.roles.directory_roles import DirectoryRolesDataCollector +from collectors.entra.roles.privileged_roles import PrivilegedRolesDataCollector + +# Users +from collectors.entra.users.users import UsersDataCollector + +# Exchange - DNS +from collectors.exchange.dns.dns_security_records import ( + DnsSecurityRecordsDataCollector, +) + +# Exchange - Audit +from collectors.exchange.audit.admin_audit_log_config import ( + AdminAuditLogConfigDataCollector, +) + +# Exchange - Organization +from collectors.exchange.organization.organization_config import ( + OrganizationConfigDataCollector, +) +from collectors.exchange.organization.owa_mailbox_policy import ( + OwaMailboxPolicyDataCollector, +) +from collectors.exchange.organization.sharing_policy import ( + SharingPolicyDataCollector, +) +from collectors.exchange.organization.transport_config import ( + TransportConfigDataCollector, +) + +# Exchange - Authentication +from collectors.exchange.authentication.dkim_signing_config import ( + DkimSigningConfigDataCollector, +) + +# Exchange - Mailbox +from collectors.exchange.mailbox.mailbox_audit import MailboxAuditDataCollector +from collectors.exchange.mailbox.mailbox_audit_actions import ( + MailboxAuditActionsDataCollector, +) +from collectors.exchange.mailbox.mailboxes import MailboxesDataCollector +from collectors.exchange.mailbox.role_assignment_policy import ( + RoleAssignmentPolicyDataCollector, +) + +# Exchange - Protection +from collectors.exchange.protection.anti_phish_policy import ( + AntiPhishPolicyDataCollector, +) +from collectors.exchange.protection.atp_policy_o365 import AtpPolicyO365DataCollector +from collectors.exchange.protection.hosted_connection_filter import ( + HostedConnectionFilterDataCollector, +) +from collectors.exchange.protection.hosted_content_filter import ( + HostedContentFilterDataCollector, +) +from collectors.exchange.protection.hosted_outbound_spam_filter import ( + HostedOutboundSpamFilterDataCollector, +) +from collectors.exchange.protection.malware_filter_policy import ( + MalwareFilterPolicyDataCollector, +) +from collectors.exchange.protection.safe_attachment_policy import ( + SafeAttachmentPolicyDataCollector, +) +from collectors.exchange.protection.safe_links_policy import ( + SafeLinksPolicyDataCollector, +) +from collectors.exchange.protection.teams_protection_policy import ( + TeamsProtectionPolicyDataCollector, +) + +# Exchange - Transport +from collectors.exchange.transport.external_in_outlook import ( + ExternalInOutlookDataCollector, +) +from collectors.exchange.transport.transport_rules import TransportRulesDataCollector + +# Compliance +from collectors.compliance.report_submission_policy import ( + ReportSubmissionPolicyDataCollector, +) + +# Registry mapping data_collector_id to collector class +DATA_COLLECTORS: dict[str, type[BaseDataCollector]] = { + # Applications + "entra.applications.apps_and_services_settings": AppsAndServicesSettingsDataCollector, + "entra.applications.forms_settings": FormsSettingsDataCollector, + "entra.applications.service_principals": ServicePrincipalsDataCollector, + # Authentication + "entra.authentication.authentication_methods": AuthenticationMethodsDataCollector, + "entra.authentication.mfa_fatigue_protection": MfaFatigueProtectionDataCollector, + "entra.authentication.mfa_registration_report": MFARegistrationReportDataCollector, + "entra.authentication.password_protection": PasswordProtectionDataCollector, + # Conditional Access + "entra.conditional_access.policies": ConditionalAccessPoliciesDataCollector, + "entra.conditional_access.legacy_auth_block": LegacyAuthBlockDataCollector, + # Devices + "entra.devices.device_management_settings": DeviceManagementSettingsDataCollector, + "entra.devices.device_registration_policy": DeviceRegistrationPolicyDataCollector, + "entra.devices.enrollment_restrictions": EnrollmentRestrictionsDataCollector, + # Domains + "entra.domains.domains": DomainsDataCollector, + "entra.domains.password_policy": PasswordPolicyDataCollector, + # Governance + "entra.governance.access_reviews": AccessReviewsDataCollector, + "entra.governance.pim_role_policies": PimRolePoliciesDataCollector, + # Groups + "entra.groups.groups": GroupsDataCollector, + # Policies + "entra.policies.activity_timeout_policy": ActivityTimeoutPolicyDataCollector, + "entra.policies.admin_consent_request_policy": AdminConsentRequestPolicyDataCollector, + "entra.policies.authorization_policy": AuthorizationPolicyDataCollector, + "entra.policies.b2b_policy": B2BPolicyDataCollector, + # Roles + "entra.roles.cloud_only_admins": CloudOnlyAdminsDataCollector, + "entra.roles.directory_roles": DirectoryRolesDataCollector, + "entra.roles.privileged_roles": PrivilegedRolesDataCollector, + # Users + "entra.users.users": UsersDataCollector, + # Exchange - DNS + "exchange.dns.dns_security_records": DnsSecurityRecordsDataCollector, + # Exchange - Audit + "exchange.audit.admin_audit_log_config": AdminAuditLogConfigDataCollector, + # Exchange - Organization + "exchange.organization.organization_config": OrganizationConfigDataCollector, + "exchange.organization.owa_mailbox_policy": OwaMailboxPolicyDataCollector, + "exchange.organization.sharing_policy": SharingPolicyDataCollector, + "exchange.organization.transport_config": TransportConfigDataCollector, + # Exchange - Authentication + "exchange.authentication.dkim_signing_config": DkimSigningConfigDataCollector, + # Exchange - Mailbox + "exchange.mailbox.mailbox_audit": MailboxAuditDataCollector, + "exchange.mailbox.mailbox_audit_actions": MailboxAuditActionsDataCollector, + "exchange.mailbox.mailboxes": MailboxesDataCollector, + "exchange.mailbox.role_assignment_policy": RoleAssignmentPolicyDataCollector, + # Exchange - Protection + "exchange.protection.anti_phish_policy": AntiPhishPolicyDataCollector, + "exchange.protection.atp_policy_o365": AtpPolicyO365DataCollector, + "exchange.protection.hosted_connection_filter": HostedConnectionFilterDataCollector, + "exchange.protection.hosted_content_filter": HostedContentFilterDataCollector, + "exchange.protection.hosted_outbound_spam_filter": HostedOutboundSpamFilterDataCollector, + "exchange.protection.malware_filter_policy": MalwareFilterPolicyDataCollector, + "exchange.protection.safe_attachment_policy": SafeAttachmentPolicyDataCollector, + "exchange.protection.safe_links_policy": SafeLinksPolicyDataCollector, + "exchange.protection.teams_protection_policy": TeamsProtectionPolicyDataCollector, + # Exchange - Transport + "exchange.transport.external_in_outlook": ExternalInOutlookDataCollector, + "exchange.transport.transport_rules": TransportRulesDataCollector, + # Compliance + "compliance.report_submission_policy": ReportSubmissionPolicyDataCollector, +} + + +def get_collector(collector_id: str) -> BaseDataCollector: + """Get a collector instance by ID.""" + collector_class = DATA_COLLECTORS.get(collector_id) + if not collector_class: + raise ValueError(f"Unknown collector: {collector_id}") + return collector_class() diff --git a/engine/collectors/sharepoint/__init__.py b/engine/collectors/sharepoint/__init__.py new file mode 100644 index 00000000..785d70e6 --- /dev/null +++ b/engine/collectors/sharepoint/__init__.py @@ -0,0 +1,10 @@ +"""SharePoint Online collectors. + +These collectors use the SharePoint REST API instead of PowerShell because +SharePoint Online PowerShell does not support client secret authentication +for app-only scenarios. + +NOTE: If certificate authentication is adopted in the future, these collectors +should be updated to use PowerShell cmdlets (Get-SPOTenant, Get-SPOSite, etc.) +for consistency with other collectors. +""" diff --git a/engine/collectors/sharepoint/spo_site.py b/engine/collectors/sharepoint/spo_site.py new file mode 100644 index 00000000..fde904ce --- /dev/null +++ b/engine/collectors/sharepoint/spo_site.py @@ -0,0 +1,44 @@ +"""SPO site collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 7.2.4 + +Connection Method: SharePoint REST API +Authentication: Client secret via MSAL (access token) + +CAVEAT: Access token authentication has not been fully tested. + It should work, but needs verification during implementation. Certificate-based + authentication may be required instead of client secret authentication. + +NOTE: This collector uses SharePoint REST API instead of PowerShell because +SharePoint Online PowerShell does not support client secret authentication. +If certificate authentication is adopted in the future, this collector should +be updated to use the Get-SPOSite cmdlet instead. + +REST Endpoints: /_api/site or SharePoint Admin API for site collections +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.sharepoint_client import SharePointClient + + +class SpoSiteDataCollector(BaseDataCollector): + """Collects SPO site settings for CIS compliance evaluation. + + This collector retrieves OneDrive and SharePoint site-specific + sharing settings for compliance evaluation. + """ + + async def collect(self, client: SharePointClient) -> dict[str, Any]: + """Collect SPO site data. + + Returns: + Dict containing: + - sites: List of site collections with settings + - onedrive_sites: OneDrive for Business sites + - site_sharing_settings: Per-site sharing configurations + """ + # TODO: Implement collector + raise NotImplementedError("Collector not yet implemented") diff --git a/engine/collectors/sharepoint/spo_sync_client_restriction.py b/engine/collectors/sharepoint/spo_sync_client_restriction.py new file mode 100644 index 00000000..99947a00 --- /dev/null +++ b/engine/collectors/sharepoint/spo_sync_client_restriction.py @@ -0,0 +1,45 @@ +"""SPO sync client restriction collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 7.3.2 + +Connection Method: SharePoint REST API +Authentication: Client secret via MSAL (access token) + +CAVEAT: Access token authentication has not been fully tested. + It should work, but needs verification during implementation. Certificate-based + authentication may be required instead of client secret authentication. + +NOTE: This collector uses SharePoint REST API instead of PowerShell because +SharePoint Online PowerShell does not support client secret authentication. +If certificate authentication is adopted in the future, this collector should +be updated to use the Get-SPOTenantSyncClientRestriction cmdlet instead. + +REST Endpoints: SharePoint Admin API for sync client restrictions +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.sharepoint_client import SharePointClient + + +class SpoSyncClientRestrictionDataCollector(BaseDataCollector): + """Collects SPO sync client restrictions for CIS compliance evaluation. + + This collector retrieves OneDrive sync restrictions for unmanaged + devices to verify proper sync controls are in place. + """ + + async def collect(self, client: SharePointClient) -> dict[str, Any]: + """Collect SPO sync client restriction data. + + Returns: + Dict containing: + - sync_client_restrictions: Sync restriction settings + - block_mac_sync: Whether Mac sync is blocked for unmanaged + - domain_guids_allowed: Allowed domain GUIDs for sync + - excluded_file_extensions: File types excluded from sync + """ + # TODO: Implement collector + raise NotImplementedError("Collector not yet implemented") diff --git a/engine/collectors/sharepoint/spo_tenant.py b/engine/collectors/sharepoint/spo_tenant.py new file mode 100644 index 00000000..1e3ae0d3 --- /dev/null +++ b/engine/collectors/sharepoint/spo_tenant.py @@ -0,0 +1,47 @@ +"""SPO tenant collector. + +CIS Microsoft 365 Foundations Benchmark Controls: + v6.0.0: 7.2.1, 7.2.2, 7.2.3, 7.2.4, 7.2.5, 7.2.6, 7.2.7, 7.2.9, 7.2.10, 7.2.11, 7.3.1 + +Connection Method: SharePoint REST API +Authentication: Client secret via MSAL (access token) + +CAVEAT: Access token authentication has not been fully tested. + It should work, but needs verification during implementation. Certificate-based + authentication may be required instead of client secret authentication. + +NOTE: This collector uses SharePoint REST API instead of PowerShell because +SharePoint Online PowerShell does not support client secret authentication. +If certificate authentication is adopted in the future, this collector should +be updated to use the Get-SPOTenant cmdlet instead. + +REST Endpoints: /_api/SPOTenant or SharePoint Admin API +""" + +from typing import Any + +from collectors.base import BaseDataCollector +from collectors.sharepoint_client import SharePointClient + + +class SpoTenantDataCollector(BaseDataCollector): + """Collects SPO tenant settings for CIS compliance evaluation. + + This collector retrieves tenant-wide SharePoint settings including + sharing, authentication, guest access, and other configurations. + """ + + async def collect(self, client: SharePointClient) -> dict[str, Any]: + """Collect SPO tenant data. + + Returns: + Dict containing: + - tenant_settings: Full tenant configuration + - legacy_auth_protocols_enabled: Legacy auth status + - azure_ad_b2b_integration_enabled: B2B integration status + - sharing_capability: External sharing capability + - default_sharing_link_type: Default sharing link type + - default_link_permission: Default link permission level + """ + # TODO: Implement collector + raise NotImplementedError("Collector not yet implemented") diff --git a/engine/collectors/sharepoint_client.py b/engine/collectors/sharepoint_client.py new file mode 100644 index 00000000..9f2ad38d --- /dev/null +++ b/engine/collectors/sharepoint_client.py @@ -0,0 +1,103 @@ +"""SharePoint REST API client. + +This module provides connectivity to SharePoint Online using REST API instead +of PowerShell because SharePoint Online PowerShell does not support client +secret authentication for app-only scenarios. + +Authentication: Client secret via MSAL -> access token for SharePoint resource + +CAVEAT: Access token authentication has not been fully tested. + It should work, but needs verification during implementation. Certificate-based + authentication may be required instead of client secret authentication. + +NOTE: If certificate authentication is adopted in the future, consider replacing +these REST API calls with PowerShell cmdlets (Get-SPOTenant, Get-SPOSite, etc.) +for consistency with other collectors. + +SharePoint REST API Reference: +- Tenant Admin API: https://{tenant}-admin.sharepoint.com/_api/ +- Site API: https://{site-url}/_api/ +""" + +from typing import Any + +import httpx +from msal import ConfidentialClientApplication + + +class SharePointClient: + """Client for SharePoint Online REST API using client secret auth.""" + + def __init__(self, tenant_id: str, client_id: str, client_secret: str, tenant_name: str): + """Initialize SharePoint client. + + Args: + tenant_id: Azure AD tenant ID + client_id: Application (client) ID + client_secret: Client secret for authentication + tenant_name: SharePoint tenant name (e.g., 'contoso' for contoso.sharepoint.com) + """ + self.tenant_id = tenant_id + self.client_id = client_id + self.client_secret = client_secret + self.tenant_name = tenant_name + self.admin_url = f"https://{tenant_name}-admin.sharepoint.com" + self._access_token: str | None = None + + self._msal_app = ConfidentialClientApplication( + client_id=client_id, + client_credential=client_secret, + authority=f"https://login.microsoftonline.com/{tenant_id}", + ) + + async def _get_access_token(self) -> str: + """Get access token for SharePoint. + + Returns: + Access token string. + + Raises: + Exception: If token acquisition fails. + """ + if self._access_token: + return self._access_token + + result = self._msal_app.acquire_token_for_client( + scopes=[f"{self.admin_url}/.default"] + ) + if "access_token" not in result: + error = result.get("error_description", result.get("error", "Unknown")) + raise Exception(f"Failed to acquire SharePoint token: {error}") + + self._access_token = result["access_token"] + return self._access_token + + async def get_tenant_settings(self) -> dict[str, Any]: + """Get SPO tenant settings via REST API. + + Returns: + Dict containing tenant configuration properties. + """ + # TODO: Implement REST API call + raise NotImplementedError("SharePoint client not yet implemented") + + async def get_site_properties(self, site_url: str) -> dict[str, Any]: + """Get site properties via REST API. + + Args: + site_url: The SharePoint site URL + + Returns: + Dict containing site properties. + """ + # TODO: Implement REST API call + raise NotImplementedError("SharePoint client not yet implemented") + + async def get_sync_client_restriction(self) -> dict[str, Any]: + """Get OneDrive sync client restriction settings. + + Returns: + Dict containing sync client restriction settings. + """ + # TODO: Implement REST API call + raise NotImplementedError("SharePoint client not yet implemented") diff --git a/engine/collectors/teams/__init__.py b/engine/collectors/teams/__init__.py new file mode 100644 index 00000000..efb77ba9 --- /dev/null +++ b/engine/collectors/teams/__init__.py @@ -0,0 +1,6 @@ +"""Microsoft Teams collectors. + +These collectors use Microsoft Teams PowerShell via the MicrosoftTeams module. +Authentication uses client secret via MSAL to obtain access tokens, +which are passed to PowerShell cmdlets via the -AccessTokens parameter. +""" diff --git a/engine/docker/powershell/Dockerfile b/engine/docker/powershell/Dockerfile new file mode 100644 index 00000000..fa427fd6 --- /dev/null +++ b/engine/docker/powershell/Dockerfile @@ -0,0 +1,11 @@ +FROM mcr.microsoft.com/powershell:7.5-mariner-2.0 + +# Install PowerShell modules for M365 management +RUN pwsh -NoProfile -Command " \ + Set-PSRepository PSGallery -InstallationPolicy Trusted; \ + Install-Module -Name ExchangeOnlineManagement -Scope AllUsers -Force; \ + Install-Module -Name MicrosoftTeams -Scope AllUsers -Force \ +" + +# Set entrypoint to PowerShell +ENTRYPOINT ["pwsh", "-NoProfile", "-NonInteractive", "-Command"] diff --git a/engine/engine/autoaudit_reports.json b/engine/engine/autoaudit_reports.json deleted file mode 100644 index 12d4100c..00000000 --- a/engine/engine/autoaudit_reports.json +++ /dev/null @@ -1,477 +0,0 @@ -{ - "CIS_GCP_1_1": { - "error": "" - }, - "CIS_GCP_1_5": { - "counts": { - "violations": 3 - }, - "id": "CIS_GCP_1_5", - "input_kind": "Identity and Access Management", - "remediation": "From Google Cloud CLI\n1. Export current IAM policy:\n gcloud projects get-iam-policy PROJECT_ID --format=json > iam.json\n2. Edit iam.json: remove roles/owner and roles/editor from any serviceAccount: bindings.\n3. Apply the updated policy:\n gcloud projects set-iam-policy PROJECT_ID iam.json", - "status": "NonCompliant", - "title": "Service accounts must not have Owner/Editor", - "verification": " From Google Cloud CLI\n1. Get the policy that you want to modify, and write it to a JSON file:\nPage 27\ngcloud projects get-iam-policy PROJECT_ID --format json > iam.json\n2. The contents of the JSON file will look similar to the following. Note that role of\nmembers group associated with each serviceaccount does not contain *Admin\nor *admin or does not match roles/editor or does not match roles/owner.\nThis recommendation is only applicable to User-Managed user-created service\naccounts. These accounts have the nomenclature:\nSERVICE_ACCOUNT_NAME@PROJECT_ID.iam.gserviceaccount.com. Note that some\nGoogle-managed, Google-created service accounts have the same naming format, and\nshould be excluded (e.g., appsdev-apps-dev-script-\nauth@system.gserviceaccount.com which needs the Owner role).", - "violations": [ - "Service account \"serviceAccount:162922847862-compute@developer.gserviceaccount.com\" must not have role \"roles/editor\"", - "Service account \"serviceAccount:162922847862@cloudservices.gserviceaccount.com\" must not have role \"roles/editor\"", - "Service account \"serviceAccount:sa-noperms@coastal-stone-470308-a0.iam.gserviceaccount.com\" must not have role \"roles/editor\"" - ] - }, - "CIS_GCP_1_6": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_1_6", - "input_kind": "Identity and Access Management", - "remediation": " 1. Using a text editor, remove the bindings with the\nroles/iam.serviceAccountUser or\nroles/iam.serviceAccountTokenCreator.\n\n2. Update the project's IAM policy:\ngcloud projects set-iam-policy PROJECT_ID iam.json ", - "status": "Compliant", - "title": "Ensure That IAM Users Are Not Assigned the Service Account User or Service Account Token Creator Roles at Project", - "verification": " To ensure IAM users are not assigned Service Account User role at the project level:\ngcloud projects get-iam-policy PROJECT_ID --format json | jq\n'.bindings[].role' | grep \"roles/iam.serviceAccountUser\"\ngcloud projects get-iam-policy PROJECT_ID --format json | jq\n'.bindings[].role' | grep \"roles/iam.serviceAccountTokenCreator\"\nThese commands should not return any output. ", - "violations": [] - }, - "CIS_GCP_1_8": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_1_8", - "input_kind": "Identity and Access Management", - "remediation": "1. Go to IAM & Admin/IAM using https://console.cloud.google.com/iam-\nadmin/iam.\n2. For any member having both Service Account Admin and Service account\nUser roles granted/assigned, click the Delete Bin icon to remove either role\nfrom the member.\nRemoval of a role should be done based on the business requirements. ", - "status": "Compliant", - "title": "Ensure That Separation of Duties Is Enforced While Assigning Service Account Related Roles to Users", - "verification": " 1. List all users and role assignments:\n2. All common users listed under Service_Account_Admin_and_User are\nassigned both the roles/iam.serviceAccountAdmin and\nroles/iam.serviceAccountUser roles. ", - "violations": [] - }, - "CIS_GCP_2_1": { - "error": "" - }, - "CIS_GCP_3_1": { - "error": "" - }, - "CIS_GCP_3_2": { - "counts": { - "violations": 1 - }, - "id": "CIS_GCP_3_2", - "input_kind": "Networking", - "remediation": " For each Google Cloud Platform project,\n1. Follow the documentation and create a non-legacy network suitable for the\norganization's requirements.\n2. Follow the documentation and delete the networks in the legacy mode. ", - "status": "NonCompliant", - "title": "Ensure Legacy Networks Do Not Exist for Older Projects", - "verification": " For each Google Cloud Platform project,\n1. Set the project name in the Google Cloud Shell:\ngcloud config set project \n2. List the networks configured in that project:\ngcloud compute networks list\nNone of the listed networks should be in the legacy mode. ", - "violations": [ - "Project network config should not contain \"LEGACY\" Networks" - ] - }, - "CIS_GCP_3_3": { - "error": "" - }, - "CIS_GCP_3_6": { - "counts": { - "violations": 1 - }, - "id": "CIS_GCP_3_6", - "input_kind": "Networking", - "remediation": "1.Update the Firewall rule with the new SOURCE_RANGE from the below command:\ngcloud compute firewall-rules update FirewallName --allow=[PROTOCOL[:PORT[-\nPORT]],...] --source-ranges=[CIDR_RANGE,...]", - "status": "NonCompliant", - "title": "Ensure That SSH Access Is Restricted From the Internet", - "verification": " gcloud compute firewall-rules list --\nformat=table'(name,direction,sourceRanges,allowed)'\nEnsure that there is no rule matching the below criteria:\n\u2022 SOURCE_RANGES is 0.0.0.0/0\n\u2022 AND DIRECTION is INGRESS\n\u2022 AND IPProtocol is tcp or ALL\n\u2022 AND PORTS is set to 22 or range containing 22 or Null (not set)\nNote:\n\u2022 When ALL TCP ports are allowed in a rule, PORT does not have any value set\n(NULL)\n\u2022 When ALL Protocols are allowed in a rule, PORT does not have any value set\n(NULL) ", - "violations": [ - "generic connections on port/s \"[\\\"22\\\", \\\"Null\\\"]\" from IP ranges \"[\\\"0.0.0.0/0\\\"]\" coming in the \"[\\\"tcp\\\", \\\"ALL\\\"]\" direction using protocol \"INGRESS\" should be avoided" - ] - }, - "CIS_GCP_3_7": { - "counts": { - "violations": 1 - }, - "id": "CIS_GCP_3_7", - "input_kind": "Networking", - "remediation": "1.Update RDP Firewall rule with new SOURCE_RANGE from the below command:\ngcloud compute firewall-rules update FirewallName --allow=[PROTOCOL[:PORT[-\nPORT]],...] --source-ranges=[CIDR_RANGE,...] ", - "status": "NonCompliant", - "title": "Ensure That RDP Access Is Restricted From the Internet", - "verification": "gcloud compute firewall-rules list --\nformat=table'(name,direction,sourceRanges,allowed)'\nEnsure that there is no rule matching the below criteria:\n\u2022 SOURCE_RANGES is 0.0.0.0/0\n\u2022 AND DIRECTION is INGRESS\n\u2022 AND IPProtocol is TCP or ALL\n\u2022 AND PORTS is set to 3389 or range containing 3389 or Null (not set)\nNote:\n\u2022 When ALL TCP ports are allowed in a rule, PORT does not have any value set\n(NULL)\n\u2022 When ALL Protocols are allowed in a rule, PORT does not have any value set\n(NULL) ", - "violations": [ - "generic connections on port/s \"[\\\"3389\\\", \\\"Null\\\"]\" from IP ranges \"[\\\"0.0.0.0/0\\\"]\" coming in the \"[\\\"tcp\\\", \\\"ALL\\\"]\" direction using protocol \"INGRESS\" should be avoided" - ] - }, - "CIS_GCP_4_1": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_4_1", - "input_kind": "Compute", - "remediation": "1. Stop the instance:\ngcloud compute instances stop \n2. Update the instance:\ngcloud compute instances set-service-account --service-\naccount=\n3. Restart the instance:\ngcloud compute instances start \n2. Update the instance:\ngcloud compute instances set-service-account --service-\naccount= --scopes [SCOPE1, SCOPE2...]\n3. Restart the instance:\ngcloud compute instances start ", - "status": "Compliant", - "title": "Ensure That Instances Are Not Configured To Use the Default Service Account With Full Access to All Cloud APIs", - "verification": "1. List the instances in your project and get details on each instance:\ngcloud compute instances list --format=json | jq -r . | \"SA Scopes:\n\\(.[].serviceAccounts[].scopes) Name: \\(.[].name) Email:\n\\(.[].serviceAccounts[].email)\"\n2. Ensure that the service account section has an email that does not match the\npattern [PROJECT_NUMBER]-compute@developer.gserviceaccount.com. ", - "violations": [] - }, - "CIS_GCP_4_3": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_4_3", - "input_kind": "Compute", - "remediation": " To block project-wide public SSH keys, set the metadata value to TRUE:\ngcloud compute instances add-metadata --metadata block-\nproject-ssh-keys=TRUE ", - "status": "Compliant", - "title": "Ensure \u201cBlock Project-Wide SSH Keys\u201d Is Enabled for VM Instances", - "verification": "1. List the instances in your project and get details on each instance:\ngcloud compute instances list --format=json\n2. Ensure key: block-project-ssh-keys is set to value: 'true'.", - "violations": [] - }, - "CIS_GCP_4_4": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_4_4", - "input_kind": "Compute", - "remediation": "1. Configure oslogin on the project:\ngcloud compute project-info add-metadata --metadata enable-oslogin=TRUE\n2. Remove instance metadata that overrides the project setting.\ngcloud compute instances remove-metadata --keys=enable-\noslogin\nOptionally, you can enable two factor authentication for OS login. For more information,\nsee: https://cloud.google.com/compute/docs/oslogin/setup-two-factor-authentication.", - "status": "Compliant", - "title": "Ensure Oslogin Is Enabled for a Project", - "verification": "1. List the instances in your project and get details on each instance:\ngcloud compute instances list --format=json\n2. Verify that the section commonInstanceMetadata has a key enable-oslogin\nset to value TRUE.\nException:\nVMs created by GKE should be excluded. These VMs have names that start with\ngke- and are labeled goog-gke-node", - "violations": [] - }, - "CIS_GCP_4_5": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_4_5", - "input_kind": "Compute", - "remediation": "Use the below command to disable\ngcloud compute instances add-metadata --zone= --\nmetadata=serial-port-enable=false\nor\ngcloud compute instances add-metadata --zone= --\nmetadata=serial-port-enable=0", - "status": "Compliant", - "title": "Ensure \u2018Enable Connecting to Serial Ports\u2019 Is Not Enabled for VM Instance", - "verification": "Ensure the below command's output shows null:\ngcloud compute instances describe --zone= --\nformat=\"json(metadata.items[].key,metadata.items[].value)\"\nor key and value properties from below command's json response are equal to\nserial-port-enable and 0 or false respectively.", - "violations": [] - }, - "CIS_GCP_4_6": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_4_6", - "input_kind": "Compute", - "remediation": "1. Use the instances export command to export the existing instance properties:\ngcloud compute instances export \\\n--project \\\n--zone \\\n--destination=\nNoteReplace the following:\nINSTANCE_NAME the name for the instance that you want to export.\nPROJECT_ID: the project ID for this request.\nZONE: the zone for this instance.\nFILE_PATH: the output path where you want to save the instance configuration file on\nyour local workstation.\n2. Use a text editor to modify this file\nReplace\ncanIpForward: true\nwith\ncanIpForward: false\n3. Run this command to import the file you just modified\ngcloud compute instances update-from-file INSTANCE_NAME \\\n--project PROJECT_ID \\\n--zone ZONE \\\n--source=FILE_PATH \\\n--most-disruptive-allowed-action=REFRESH\nIf the update request is valid and the required resources are available, the instance\nupdate process begins. You can monitor the status of this operation by viewing the audit\nlogs.\nThis update requires only a REFRESH not a full restart.", - "status": "Compliant", - "title": "Ensure That IP Forwarding Is Not Enabled on Instances", - "verification": "1. List all instances:\ngcloud compute instances list --format='table(name,canIpForward)'\n2. Ensure that CAN_IP_FORWARD column in the output of above command does not\ncontain True for any VM instance.", - "violations": [] - }, - "CIS_GCP_4_8": { - "error": "" - }, - "CIS_GCP_4_9": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_4_9", - "input_kind": "Compute", - "remediation": "1. Describe the instance properties:\ngcloud compute instances describe --zone=\n2. Identify the access config name that contains the external IP address. This\naccess config appears in the following format:\nnetworkInterfaces:\n- accessConfigs:\n- kind: compute#accessConfig\nname: External NAT\nnatIP: 130.211.181.55\ntype: ONE_TO_ONE_NAT\n3. Delete the access config.\ngcloud compute instances delete-access-config --zone= -\n-access-config-name \nIn the above example, the ACCESS_CONFIG_NAME is External NAT. The name of your\naccess config might be different ", - "status": "Compliant", - "title": "Ensure That Compute Instances Do Not Have Public IPAddresses", - "verification": "gcloud compute instances list --format=json\n1. The output should not contain an accessConfigs section under\nnetworkInterfaces. Note that the natIP value is present only for instances that\nare running or for instances that are stopped but have a static IP address. For\ninstances that are stopped and are configured to have an ephemeral public IP\naddress, the natIP field will not be present. Example output:\nnetworkInterfaces:\n- accessConfigs:\n- kind: compute#accessConfig\nname: External NAT\nnetworkTier: STANDARD\ntype: ONE_TO_ONE_NAT", - "violations": [] - }, - "CIS_GCP_4_11": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_4_11", - "input_kind": "Compute", - "remediation": "Create a new instance with Confidential Compute enabled.\ngcloud compute instances create --zone --\nconfidential-compute --maintenance-policy=TERMINATE", - "status": "Compliant", - "title": "Ensure That Compute Instances Have Confidential Computing Enabled", - "verification": "1. List the instances in your project and get details on each instance:\ngcloud compute instances list --format=json\n2. Ensure that enableConfidentialCompute is set to true for all instances with\nmachine type starting with \"n2d-\".\nconfidentialInstanceConfig:\nenableConfidentialCompute: true", - "violations": [] - }, - "CIS_GCP_5_1": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_5_1", - "input_kind": "Storage", - "remediation": "Remove allUsers and allAuthenticatedUsers access.\ngsutil iam ch -d allUsers gs://BUCKET_NAME\ngsutil iam ch -d allAuthenticatedUsers gs://BUCKET_NAME ", - "status": "Compliant", - "title": "Ensure That Cloud Storage Bucket Is Not Anonymously or Publicly Accessible", - "verification": " 1. List all buckets in a project\ngsutil ls\n2. Check the IAM Policy for each bucket:\ngsutil iam get gs://BUCKET_NAME\nNo role should contain allUsers and/or allAuthenticatedUsers as a member.", - "violations": [] - }, - "CIS_GCP_6_1_2": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_1_2", - "input_kind": "Cloud SQL", - "remediation": "1. List all Cloud SQL database Instances\ngcloud sql instances list\n2. Configure the skip_show_database database flag for every Cloud SQL Mysql\ndatabase instance using the below command.\ngcloud sql instances patch --database-flags\nskip_show_database=on", - "status": "Compliant", - "title": "MySQL: Ensure 'skip_show_database' flag is ON", - "verification": "1. List all Cloud SQL database Instances\ngcloud sql instances list\n2. Ensure the below command returns on for every Cloud SQL Mysql database\ninstance \ngcloud sql instances describe --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"skip_show_database\")|.value' ", - "violations": [] - }, - "CIS_GCP_6_1_3": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_1_3", - "input_kind": "Cloud SQL", - "remediation": "1. List all Cloud SQL database instances using the following command:\ngcloud sql instances list\n2. Configure the local_infile database flag for every Cloud SQL Mysql database\ninstance using the below command:\ngcloud sql instances patch --database-flags local_infile=off", - "status": "Compliant", - "title": "MySQL: Ensure 'local_infile' flag is OFF", - "verification": "1. List all Cloud SQL database instances:\ngcloud sql instances list\n2. Ensure the below command returns off for every Cloud SQL MySQL database\ninstance.\ngcloud sql instances describe --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"local_infile\")|.value' ", - "violations": [] - }, - "CIS_GCP_6_2_1": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_2_1", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the log_error_verbosity database flag for every Cloud SQL PosgreSQL\ndatabase instance using the below command.\ngcloud sql instances patch INSTANCE_NAME --database-flags\nlog_error_verbosity=", - "status": "Compliant", - "title": "PostgreSQL: 'log_error_verbosity' DEFAULT or stricter", - "verification": "1. Use the below command for every Cloud SQL PostgreSQL database instance to\nverify the value of log_error_verbosity\ngcloud sql instances describe [INSTANCE_NAME] --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"log_error_verbosity\")|.value'\nIn the output, database flags are listed under the settings as the collection\ndatabaseFlags.", - "violations": [] - }, - "CIS_GCP_6_2_2": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_2_2", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the log_connections database flag for every Cloud SQL PosgreSQL\ndatabase instance using the below command.\ngcloud sql instances patch --database-flags\n\"log_connections\"=on", - "status": "Compliant", - "title": "PostgreSQL: 'log_connections' = on", - "verification": "1. Ensure the below command returns on for every Cloud SQL PostgreSQL\ndatabase instance:\ngcloud sql instances describe [INSTANCE_NAME] --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"log_connections\")|.value'\nIn the output, database flags are listed under the settings as the collection\ndatabaseFlags.", - "violations": [] - }, - "CIS_GCP_6_2_3": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_2_3", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the log_disconnections database flag for every Cloud SQL\nPosgreSQL database instance using the below command:\ngcloud sql instances patch --database-flags\nlog_disconnections=on", - "status": "Compliant", - "title": "PostgreSQL: 'log_disconnections' = on", - "verification": "1. Ensure the below command returns on for every Cloud SQL PostgreSQL\ndatabase instance:\ngcloud sql instances list --format=json | jq '.[].settings.databaseFlags[] |\nselect(.name==\"log_disconnections\")|.value", - "violations": [] - }, - "CIS_GCP_6_2_4": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_2_4", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the log_statement database flag for every Cloud SQL PosgreSQL\ndatabase instance using the below command.\ngcloud sql instances patch --database-flags\nlog_statement=", - "status": "Compliant", - "title": "PostgreSQL: 'log_statement' set appropriately (ddl)", - "verification": "1. Use the below command for every Cloud SQL PostgreSQL database instance to\nverify the value of log_statement\ngcloud sql instances list --format=json | jq '.[].settings.databaseFlags[] |\nselect(.name==\"log_statement\")|.value'", - "violations": [] - }, - "CIS_GCP_6_2_5": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_2_5", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the log_min_messages database flag for every Cloud SQL\nPosgreSQL database instance using the below command.\ngcloud sql instances patch --database-flags\nlog_min_messages=", - "status": "Compliant", - "title": "PostgreSQL: 'log_min_messages' >= WARNING", - "verification": "1. Use the below command for every Cloud SQL PostgreSQL database instance to\nverify that the value of log_min_messages is set to warning or higher .\ngcloud sql instances describe [INSTANCE_NAME] --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"log_min_messages\")|.value'", - "violations": [] - }, - "CIS_GCP_6_2_6": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_2_6", - "input_kind": "Cloud SQL", - "remediation": "from Google Cloud CLI\n1. Configure the log_min_error_statement database flag for every Cloud SQL\nPosgreSQL database instance using the below command.\ngcloud sql instances patch --database-flags\nlog_min_error_statement=", - "status": "Compliant", - "title": "PostgreSQL: 'log_min_error_statement' >= ERROR", - "verification": "1. Use the below command for every Cloud SQL PostgreSQL database instance to\nverify the value of log_min_error_statement is set to ERROR or stricter.\ngcloud sql instances describe --format=json | jq\n'.[].settings.databaseFlags[] |\nselect(.name==\"log_min_error_statement\")|.value'\nIn the output, database flags are listed under the settings as the collection\ndatabaseFlags.", - "violations": [] - }, - "CIS_GCP_6_2_7": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_2_7", - "input_kind": "Cloud SQL", - "remediation": "1. List all Cloud SQL database instances using the following command:\ngcloud sql instances list\n2. Configure the log_min_duration_statement flag for every Cloud SQL\nPosgreSQL database instance using the below command:\ngcloud sql instances patch --database-flags\nlog_min_duration_statement=-1", - "status": "Compliant", - "title": "PostgreSQL: 'log_min_duration_statement' = -1 (disabled)", - "verification": "1. Use the below command for every Cloud SQL PostgreSQL database instance to\nverify the value of log_min_duration_statement is set to -1.\ngcloud sql instances describe --format=json| jq\n'.settings.databaseFlags[] |\nselect(.name==\"log_min_duration_statement\")|.value'\nIn the output, database flags are listed under the settings as the collection\ndatabaseFlags.", - "violations": [] - }, - "CIS_GCP_6_2_8": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_2_8", - "input_kind": "Cloud SQL", - "remediation": "Run the below command by providing to enable\ncloudsql.enable_pgaudit flag.\ngcloud sql instances patch --database-flags\ncloudsql.enable_pgaudit=on\nNote: RESTART is required to get this configuration in effect.", - "status": "Compliant", - "title": "PostgreSQL: 'cloudsql.enable_pgaudit' = on", - "verification": "Run the command by providing . Ensure the value of the flag is on.\ngcloud sql instances describe --format=\"json\" | jq\n'.settings|.|.databaseFlags[]|select(.name==\"cloudsql.enable_pgaudit\")|.value", - "violations": [] - }, - "CIS_GCP_6_3_1": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_3_1", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the external scripts enabled database flag for every Cloud SQL\nSQL Server database instance using the below command.\ngcloud sql instances patch --database-flags \"external scripts\nenabled\"=off", - "status": "Compliant", - "title": "SQL Server: 'external scripts enabled' = off", - "verification": "1. Ensure the below command returns off for every Cloud SQL SQL Server\ndatabase instance\ngcloud sql instances describe --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"external scripts\nenabled\")|.value' \nIn the output, database flags are listed under the settings as the collection\ndatabaseFlags.", - "violations": [] - }, - "CIS_GCP_6_3_2": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_3_2", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the cross db ownership chaining database flag for every Cloud\nSQL SQL Server database instance using the below command:\ngcloud sql instances patch --database-flags \"cross db\nownership chaining\"=off", - "status": "Compliant", - "title": "SQL Server: 'cross db ownership chaining' = off", - "verification": "1. Ensure the below command returns off for every Cloud SQL SQL Server\ndatabase instance:\ngcloud sql instances describe --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"cross db ownership\nchaining\")|.value'\nIn the output, database flags are listed under the settings as the collection\ndatabaseFlags.", - "violations": [] - }, - "CIS_GCP_6_3_3": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_3_3", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the user connections database flag for every Cloud SQL SQL\nServer database instance using the below command.\ngcloud sql instances patch --database-flags \"user\nconnections=[0-32,767]\"", - "status": "Compliant", - "title": "SQL Server: 'user connections' = 0 (non-limiting)", - "verification": "1. Ensure the below command returns a value of 0, for every Cloud SQL SQL\nServer database instance.\ngcloud sql instances describe --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"user connections\")|.value'", - "violations": [] - }, - "CIS_GCP_6_3_4": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_3_4", - "input_kind": "Cloud SQL", - "remediation": "1. List all Cloud SQL database Instances\ngcloud sql instances list\n2. Clear the user options database flag for every Cloud SQL SQL Server\ndatabase instance using either of the below commands.\nClearing all flags to their default value\ngcloud sql instances patch --clear-database-flags", - "status": "Compliant", - "title": "SQL Server: 'user options' not configured", - "verification": "1. Ensure the below command returns empty result for every Cloud SQL SQL\nServer database instance\ngcloud sql instances describe --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"user options\")|.value'\nIn the output, database flags are listed under the settings as the collection\ndatabaseFlags.", - "violations": [] - }, - "CIS_GCP_6_3_5": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_3_5", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the remote access database flag for every Cloud SQL SQL Server\ndatabase instance using the below command\ngcloud sql instances patch --database-flags \"remote\naccess\"=off", - "status": "Compliant", - "title": "SQL Server: 'remote access' = off", - "verification": "1. Ensure the below command returns off for every Cloud SQL SQL Server\ndatabase instance\ngcloud sql instances describe --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"remote access\")|.value'\nIn the output, database flags are listed under the settings as the collection\ndatabaseFlags.", - "violations": [] - }, - "CIS_GCP_6_3_6": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_3_6", - "input_kind": "Cloud SQL", - "remediation": "1. Configure the 3625 database flag for every Cloud SQL SQL Server database\ninstance using the below command.\ngcloud sql instances patch --database-flags \"3625=on\"", - "status": "Compliant", - "title": "SQL Server: '3625' trace flag = on", - "verification": "1. Ensure the below command returns on for every Cloud SQL SQL Server\ndatabase instance\ngcloud sql instances describe --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"3625\")|.value'", - "violations": [] - }, - "CIS_GCP_6_3_7": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_3_7", - "input_kind": "Cloud SQL", - "remediation": "1. If any Cloud SQL for SQL Server instance has the database flag contained\ndatabase authentication set to 'on', then change it to 'off' using the below\ncommand:\ngcloud sql instances patch --database-flags \"contained\ndatabase authentication=off\"", - "status": "Compliant", - "title": "SQL Server: 'contained database authentication' = off", - "verification": "1. Ensure the below command returns off for any Cloud SQL for SQL Server\ndatabase instance.\ngcloud sql instances describe --format=json | jq\n'.settings.databaseFlags[] | select(.name==\"contained database\nauthentication\")|.value'", - "violations": [] - }, - "CIS_GCP_6_4": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_4", - "input_kind": "Cloud SQL", - "remediation": "To enforce SSL encryption for an instance run the command:\ngcloud sql instances patch INSTANCE_NAME --ssl-mode= ENCRYPTED_ONLY\nNote:\nRESTART is required for type MySQL Generation 1 Instances (backendType:\nFIRST_GEN) to get this configuration in effect", - "status": "Compliant", - "title": "Ensure Cloud SQL requires SSL for all incoming connections", - "verification": "1. Get the detailed configuration for every SQL database instance using the\nfollowing command:\ngcloud sql instances list --format=json\nEnsure that section settings: ipConfiguration has the parameter sslMode set to\nENCRYPTED_ONLY", - "violations": [] - }, - "CIS_GCP_6_5": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_5", - "input_kind": "Cloud SQL", - "remediation": "Update the authorized network list by dropping off any addresses.\ngcloud sql instances patch --authorized-\nnetworks=IP_ADDR1,IP_ADDR2...", - "status": "Compliant", - "title": "Ensure Cloud SQL does not implicitly whitelist all public IPs", - "verification": "1. Get detailed configuration for every Cloud SQL database instance.\nThe Cloud SQL database instance would not be available to public IP addresses.\ngcloud sql instances list --format=json\nEnsure that the section settings: ipConfiguration : authorizedNetworks does\nnot have any parameter value containing 0.0.0.0/0.", - "violations": [] - }, - "CIS_GCP_6_6": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_6", - "input_kind": "Cloud SQL", - "remediation": "1. For every instance remove its public IP and assign a private IP instead:\ngcloud sql instances patch --network= --no-\nassign-ip\n2. Confirm the changes using the following command::\ngcloud sql instances describe ", - "status": "Compliant", - "title": "Ensure Cloud SQL instances do not have public IPs", - "verification": "1. List all Cloud SQL database instances using the following command:\ngcloud sql instances list\n2. For every instance of type instanceType: CLOUD_SQL_INSTANCE with\nbackendType: SECOND_GEN, get detailed configuration. Ignore instances of type\nREAD_REPLICA_INSTANCE because these instances inherit their settings from the\nprimary instance. Also, note that first generation instances cannot be configured\nto have a private IP address.\ngcloud sql instances describe \n3. Ensure that the setting ipAddresses has an IP address configured of type:\nPRIVATE and has no IP address of type: PRIMARY. PRIMARY IP addresses are\npublic addresses. An instance can have both a private and public address at the\nsame time. Note also that you cannot use private IP with First Generation\ninstances.", - "violations": [] - }, - "CIS_GCP_6_7": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_6_7", - "input_kind": "Cloud SQL", - "remediation": "1. List all Cloud SQL database instances using the following command:\ngcloud sql instances list --format=json | jq '. | map(select(.instanceType !=\n\"READ_REPLICA_INSTANCE\")) | .[].name'\n2. Enable Automated backups for every Cloud SQL database instance using the\nbelow command:\ngcloud sql instances patch --backup-start-time <[HH:MM]>\nThe backup-start-time parameter is specified in 24-hour time, in the UTC\u00b100 time\nzone, and specifies the start of a 4-hour backup window. Backups can start any time\nduring the backup window.", - "status": "Compliant", - "title": "Ensure Cloud SQL automated backups are enabled", - "verification": "1. List all Cloud SQL database instances using the following command:\ngcloud sql instances list --format=json | jq '. | map(select(.instanceType !=\n\"READ_REPLICA_INSTANCE\")) | .[].name'\n2. Ensure that the below command returns True for every Cloud SQL database\ninstance.\ngcloud sql instances describe --\nformat=\"value('Enabled':settings.backupConfiguration.enabled)\"", - "violations": [] - }, - "CIS_GCP_7_1": { - "error": "No result in OPA output" - }, - "CIS_GCP_7_3": { - "counts": { - "violations": 0 - }, - "id": "CIS_GCP_7_3", - "input_kind": "BigQuery", - "remediation": "The default CMEK for existing data sets can be updated by specifying the default key in\nthe EncryptionConfiguration.kmsKeyName field when calling the datasets.insert\nor datasets.patch methods", - "status": "Compliant", - "title": "Ensure a default CMEK is specified for all BigQuery datasets", - "verification": "List all dataset names\nbq ls\nUse the following command to view each dataset details.\nbq show \nVerify the kmsKeyName is present.", - "violations": [] - }, - "CIS_GCP_8_1": { - "error": "No result in OPA output" - } -} \ No newline at end of file diff --git a/engine/engine/GCPAccess.py b/engine/legacy/engine/GCPAccess.py similarity index 100% rename from engine/engine/GCPAccess.py rename to engine/legacy/engine/GCPAccess.py diff --git a/engine/engine/Helpers.rego b/engine/legacy/engine/Helpers.rego similarity index 100% rename from engine/engine/Helpers.rego rename to engine/legacy/engine/Helpers.rego diff --git a/engine/engine/aggregator.py b/engine/legacy/engine/aggregator.py similarity index 89% rename from engine/engine/aggregator.py rename to engine/legacy/engine/aggregator.py index f88336b5..0e76dc46 100644 --- a/engine/engine/aggregator.py +++ b/engine/legacy/engine/aggregator.py @@ -1,8 +1,8 @@ import subprocess, json, sys from pathlib import Path -RULES = Path("engine/rules") -CONFIGS = Path("engine/test-configs") +RULES = Path("engine/legacy/rules-gcp") +CONFIGS = Path("engine/legacy/test-configs") policy_map = { "CIS_GCP_1_1.rego": "iam_policy.json", @@ -67,7 +67,19 @@ pkg = Path(rego).stem query = f"data.AutoAudit_tester.rules.{pkg}.report" - cmd = ["opa", "eval", "-f", "json", "-i", str(CONFIGS/config), "-d" , "engine/engine", "-d", str(RULES/rego), query] + cmd = [ + "opa", + "eval", + "-f", + "json", + "-i", + str(CONFIGS / config), + "-d", + "engine/legacy/engine", + "-d", + str(RULES / rego), + query, + ] proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode != 0: reports[pkg] = {"error": proc.stderr} @@ -86,7 +98,7 @@ except Exception as e: reports[pkg] = {"error": f"Failed to parse OPA output: {e}"} -output_path = "engine/engine/autoaudit_reports.json" +output_path = "engine/legacy/engine/autoaudit_reports.json" with open(output_path, "w") as f: json.dump(reports, f, indent=2) print(f"Wrote {output_path}") \ No newline at end of file diff --git a/engine/engine/evaluation_reporter.py b/engine/legacy/engine/evaluation_reporter.py similarity index 100% rename from engine/engine/evaluation_reporter.py rename to engine/legacy/engine/evaluation_reporter.py diff --git a/engine/engine/json_to_pdf.py b/engine/legacy/engine/json_to_pdf.py similarity index 100% rename from engine/engine/json_to_pdf.py rename to engine/legacy/engine/json_to_pdf.py diff --git a/engine/engine/risk_rating.py b/engine/legacy/engine/risk_rating.py similarity index 100% rename from engine/engine/risk_rating.py rename to engine/legacy/engine/risk_rating.py diff --git a/engine/engine/rule_helpers.py b/engine/legacy/engine/rule_helpers.py similarity index 100% rename from engine/engine/rule_helpers.py rename to engine/legacy/engine/rule_helpers.py diff --git a/engine/engine/trigger/trigger1.txt b/engine/legacy/engine/trigger/trigger1.txt similarity index 100% rename from engine/engine/trigger/trigger1.txt rename to engine/legacy/engine/trigger/trigger1.txt diff --git a/engine/Rules-Azure/1.1.1.json b/engine/legacy/rules-azure/1.1.1.json similarity index 100% rename from engine/Rules-Azure/1.1.1.json rename to engine/legacy/rules-azure/1.1.1.json diff --git a/engine/Rules-Azure/1.1.2.json b/engine/legacy/rules-azure/1.1.2.json similarity index 100% rename from engine/Rules-Azure/1.1.2.json rename to engine/legacy/rules-azure/1.1.2.json diff --git a/engine/Rules-Azure/1.1.3.json b/engine/legacy/rules-azure/1.1.3.json similarity index 100% rename from engine/Rules-Azure/1.1.3.json rename to engine/legacy/rules-azure/1.1.3.json diff --git a/engine/Rules-Azure/1.1.4.json b/engine/legacy/rules-azure/1.1.4.json similarity index 100% rename from engine/Rules-Azure/1.1.4.json rename to engine/legacy/rules-azure/1.1.4.json diff --git a/engine/Rules-Azure/1.2.1.json b/engine/legacy/rules-azure/1.2.1.json similarity index 100% rename from engine/Rules-Azure/1.2.1.json rename to engine/legacy/rules-azure/1.2.1.json diff --git a/engine/Rules-Azure/1.2.2.json b/engine/legacy/rules-azure/1.2.2.json similarity index 100% rename from engine/Rules-Azure/1.2.2.json rename to engine/legacy/rules-azure/1.2.2.json diff --git a/engine/Rules-Azure/1.3.1.json b/engine/legacy/rules-azure/1.3.1.json similarity index 100% rename from engine/Rules-Azure/1.3.1.json rename to engine/legacy/rules-azure/1.3.1.json diff --git a/engine/Rules-Azure/1.3.2.json b/engine/legacy/rules-azure/1.3.2.json similarity index 100% rename from engine/Rules-Azure/1.3.2.json rename to engine/legacy/rules-azure/1.3.2.json diff --git a/engine/Rules-Azure/1.3.3.json b/engine/legacy/rules-azure/1.3.3.json similarity index 100% rename from engine/Rules-Azure/1.3.3.json rename to engine/legacy/rules-azure/1.3.3.json diff --git a/engine/Rules-Azure/1.3.4.json b/engine/legacy/rules-azure/1.3.4.json similarity index 100% rename from engine/Rules-Azure/1.3.4.json rename to engine/legacy/rules-azure/1.3.4.json diff --git a/engine/Rules-Azure/1.3.5.json b/engine/legacy/rules-azure/1.3.5.json similarity index 100% rename from engine/Rules-Azure/1.3.5.json rename to engine/legacy/rules-azure/1.3.5.json diff --git a/engine/Rules-Azure/1.3.6.json b/engine/legacy/rules-azure/1.3.6.json similarity index 100% rename from engine/Rules-Azure/1.3.6.json rename to engine/legacy/rules-azure/1.3.6.json diff --git a/engine/Rules-Azure/1.3.7.json b/engine/legacy/rules-azure/1.3.7.json similarity index 100% rename from engine/Rules-Azure/1.3.7.json rename to engine/legacy/rules-azure/1.3.7.json diff --git a/engine/Rules-Azure/1.3.8.json b/engine/legacy/rules-azure/1.3.8.json similarity index 100% rename from engine/Rules-Azure/1.3.8.json rename to engine/legacy/rules-azure/1.3.8.json diff --git a/engine/Rules-Azure/2.1.1.json b/engine/legacy/rules-azure/2.1.1.json similarity index 100% rename from engine/Rules-Azure/2.1.1.json rename to engine/legacy/rules-azure/2.1.1.json diff --git a/engine/Rules-Azure/2.1.10.json b/engine/legacy/rules-azure/2.1.10.json similarity index 100% rename from engine/Rules-Azure/2.1.10.json rename to engine/legacy/rules-azure/2.1.10.json diff --git a/engine/Rules-Azure/2.1.11.json b/engine/legacy/rules-azure/2.1.11.json similarity index 100% rename from engine/Rules-Azure/2.1.11.json rename to engine/legacy/rules-azure/2.1.11.json diff --git a/engine/Rules-Azure/2.1.12.json b/engine/legacy/rules-azure/2.1.12.json similarity index 100% rename from engine/Rules-Azure/2.1.12.json rename to engine/legacy/rules-azure/2.1.12.json diff --git a/engine/Rules-Azure/2.1.13.json b/engine/legacy/rules-azure/2.1.13.json similarity index 100% rename from engine/Rules-Azure/2.1.13.json rename to engine/legacy/rules-azure/2.1.13.json diff --git a/engine/Rules-Azure/2.1.14.json b/engine/legacy/rules-azure/2.1.14.json similarity index 100% rename from engine/Rules-Azure/2.1.14.json rename to engine/legacy/rules-azure/2.1.14.json diff --git a/engine/Rules-Azure/2.1.2.json b/engine/legacy/rules-azure/2.1.2.json similarity index 100% rename from engine/Rules-Azure/2.1.2.json rename to engine/legacy/rules-azure/2.1.2.json diff --git a/engine/Rules-Azure/2.1.3.json b/engine/legacy/rules-azure/2.1.3.json similarity index 100% rename from engine/Rules-Azure/2.1.3.json rename to engine/legacy/rules-azure/2.1.3.json diff --git a/engine/Rules-Azure/2.1.4.json b/engine/legacy/rules-azure/2.1.4.json similarity index 100% rename from engine/Rules-Azure/2.1.4.json rename to engine/legacy/rules-azure/2.1.4.json diff --git a/engine/Rules-Azure/2.1.5.json b/engine/legacy/rules-azure/2.1.5.json similarity index 100% rename from engine/Rules-Azure/2.1.5.json rename to engine/legacy/rules-azure/2.1.5.json diff --git a/engine/Rules-Azure/2.1.6.json b/engine/legacy/rules-azure/2.1.6.json similarity index 100% rename from engine/Rules-Azure/2.1.6.json rename to engine/legacy/rules-azure/2.1.6.json diff --git a/engine/Rules-Azure/2.1.7.json b/engine/legacy/rules-azure/2.1.7.json similarity index 100% rename from engine/Rules-Azure/2.1.7.json rename to engine/legacy/rules-azure/2.1.7.json diff --git a/engine/Rules-Azure/2.1.8.json b/engine/legacy/rules-azure/2.1.8.json similarity index 100% rename from engine/Rules-Azure/2.1.8.json rename to engine/legacy/rules-azure/2.1.8.json diff --git a/engine/Rules-Azure/2.1.9.json b/engine/legacy/rules-azure/2.1.9.json similarity index 100% rename from engine/Rules-Azure/2.1.9.json rename to engine/legacy/rules-azure/2.1.9.json diff --git a/engine/Rules-Azure/2.2.1.json b/engine/legacy/rules-azure/2.2.1.json similarity index 100% rename from engine/Rules-Azure/2.2.1.json rename to engine/legacy/rules-azure/2.2.1.json diff --git a/engine/Rules-Azure/2.4.1.json b/engine/legacy/rules-azure/2.4.1.json similarity index 100% rename from engine/Rules-Azure/2.4.1.json rename to engine/legacy/rules-azure/2.4.1.json diff --git a/engine/Rules-Azure/2.4.2.json b/engine/legacy/rules-azure/2.4.2.json similarity index 100% rename from engine/Rules-Azure/2.4.2.json rename to engine/legacy/rules-azure/2.4.2.json diff --git a/engine/Rules-Azure/2.4.3.json b/engine/legacy/rules-azure/2.4.3.json similarity index 100% rename from engine/Rules-Azure/2.4.3.json rename to engine/legacy/rules-azure/2.4.3.json diff --git a/engine/Rules-Azure/2.4.4.json b/engine/legacy/rules-azure/2.4.4.json similarity index 100% rename from engine/Rules-Azure/2.4.4.json rename to engine/legacy/rules-azure/2.4.4.json diff --git a/engine/Rules-Azure/3.1.1.json b/engine/legacy/rules-azure/3.1.1.json similarity index 100% rename from engine/Rules-Azure/3.1.1.json rename to engine/legacy/rules-azure/3.1.1.json diff --git a/engine/Rules-Azure/3.2.1.json b/engine/legacy/rules-azure/3.2.1.json similarity index 100% rename from engine/Rules-Azure/3.2.1.json rename to engine/legacy/rules-azure/3.2.1.json diff --git a/engine/Rules-Azure/3.2.2.json b/engine/legacy/rules-azure/3.2.2.json similarity index 100% rename from engine/Rules-Azure/3.2.2.json rename to engine/legacy/rules-azure/3.2.2.json diff --git a/engine/Rules-Azure/3.3.1.json b/engine/legacy/rules-azure/3.3.1.json similarity index 100% rename from engine/Rules-Azure/3.3.1.json rename to engine/legacy/rules-azure/3.3.1.json diff --git a/engine/Rules-Azure/5.1.1.json b/engine/legacy/rules-azure/5.1.1.json similarity index 100% rename from engine/Rules-Azure/5.1.1.json rename to engine/legacy/rules-azure/5.1.1.json diff --git a/engine/Rules-Azure/5.1.2.json b/engine/legacy/rules-azure/5.1.2.json similarity index 100% rename from engine/Rules-Azure/5.1.2.json rename to engine/legacy/rules-azure/5.1.2.json diff --git a/engine/Rules-Azure/5.1.3.json b/engine/legacy/rules-azure/5.1.3.json similarity index 100% rename from engine/Rules-Azure/5.1.3.json rename to engine/legacy/rules-azure/5.1.3.json diff --git a/engine/Rules-Azure/5.1.4.json b/engine/legacy/rules-azure/5.1.4.json similarity index 100% rename from engine/Rules-Azure/5.1.4.json rename to engine/legacy/rules-azure/5.1.4.json diff --git a/engine/Rules-Azure/5.1.5.json b/engine/legacy/rules-azure/5.1.5.json similarity index 100% rename from engine/Rules-Azure/5.1.5.json rename to engine/legacy/rules-azure/5.1.5.json diff --git a/engine/Rules-Azure/5.1.6.json b/engine/legacy/rules-azure/5.1.6.json similarity index 100% rename from engine/Rules-Azure/5.1.6.json rename to engine/legacy/rules-azure/5.1.6.json diff --git a/engine/Rules-Azure/5.1.7.json b/engine/legacy/rules-azure/5.1.7.json similarity index 100% rename from engine/Rules-Azure/5.1.7.json rename to engine/legacy/rules-azure/5.1.7.json diff --git a/engine/Rules-Azure/5.1.8.json b/engine/legacy/rules-azure/5.1.8.json similarity index 100% rename from engine/Rules-Azure/5.1.8.json rename to engine/legacy/rules-azure/5.1.8.json diff --git a/engine/Rules-Azure/5.2.1.json b/engine/legacy/rules-azure/5.2.1.json similarity index 100% rename from engine/Rules-Azure/5.2.1.json rename to engine/legacy/rules-azure/5.2.1.json diff --git a/engine/Rules-Azure/5.2.2.3.json b/engine/legacy/rules-azure/5.2.2.3.json similarity index 100% rename from engine/Rules-Azure/5.2.2.3.json rename to engine/legacy/rules-azure/5.2.2.3.json diff --git a/engine/Rules-Azure/5.2.2.json b/engine/legacy/rules-azure/5.2.2.json similarity index 100% rename from engine/Rules-Azure/5.2.2.json rename to engine/legacy/rules-azure/5.2.2.json diff --git a/engine/Rules-Azure/5.2.3.json b/engine/legacy/rules-azure/5.2.3.json similarity index 100% rename from engine/Rules-Azure/5.2.3.json rename to engine/legacy/rules-azure/5.2.3.json diff --git a/engine/Rules-Azure/5.2.4.json b/engine/legacy/rules-azure/5.2.4.json similarity index 100% rename from engine/Rules-Azure/5.2.4.json rename to engine/legacy/rules-azure/5.2.4.json diff --git a/engine/Rules-Azure/5.3.1.json b/engine/legacy/rules-azure/5.3.1.json similarity index 100% rename from engine/Rules-Azure/5.3.1.json rename to engine/legacy/rules-azure/5.3.1.json diff --git a/engine/Rules-Azure/5.3.2.json b/engine/legacy/rules-azure/5.3.2.json similarity index 100% rename from engine/Rules-Azure/5.3.2.json rename to engine/legacy/rules-azure/5.3.2.json diff --git a/engine/Rules-Azure/5.3.3.json b/engine/legacy/rules-azure/5.3.3.json similarity index 100% rename from engine/Rules-Azure/5.3.3.json rename to engine/legacy/rules-azure/5.3.3.json diff --git a/engine/Rules-Azure/5.3.4.json b/engine/legacy/rules-azure/5.3.4.json similarity index 100% rename from engine/Rules-Azure/5.3.4.json rename to engine/legacy/rules-azure/5.3.4.json diff --git a/engine/Rules-Azure/5.3.5.json b/engine/legacy/rules-azure/5.3.5.json similarity index 100% rename from engine/Rules-Azure/5.3.5.json rename to engine/legacy/rules-azure/5.3.5.json diff --git a/engine/Rules-Azure/6.1.1.json b/engine/legacy/rules-azure/6.1.1.json similarity index 100% rename from engine/Rules-Azure/6.1.1.json rename to engine/legacy/rules-azure/6.1.1.json diff --git a/engine/Rules-Azure/6.1.2.json b/engine/legacy/rules-azure/6.1.2.json similarity index 100% rename from engine/Rules-Azure/6.1.2.json rename to engine/legacy/rules-azure/6.1.2.json diff --git a/engine/Rules-Azure/6.1.3.json b/engine/legacy/rules-azure/6.1.3.json similarity index 100% rename from engine/Rules-Azure/6.1.3.json rename to engine/legacy/rules-azure/6.1.3.json diff --git a/engine/Rules-Azure/6.2.1.json b/engine/legacy/rules-azure/6.2.1.json similarity index 100% rename from engine/Rules-Azure/6.2.1.json rename to engine/legacy/rules-azure/6.2.1.json diff --git a/engine/Rules-Azure/6.2.2.json b/engine/legacy/rules-azure/6.2.2.json similarity index 100% rename from engine/Rules-Azure/6.2.2.json rename to engine/legacy/rules-azure/6.2.2.json diff --git a/engine/Rules-Azure/6.2.3.json b/engine/legacy/rules-azure/6.2.3.json similarity index 100% rename from engine/Rules-Azure/6.2.3.json rename to engine/legacy/rules-azure/6.2.3.json diff --git a/engine/Rules-Azure/6.3.1.json b/engine/legacy/rules-azure/6.3.1.json similarity index 100% rename from engine/Rules-Azure/6.3.1.json rename to engine/legacy/rules-azure/6.3.1.json diff --git a/engine/Rules-Azure/6.5.1.json b/engine/legacy/rules-azure/6.5.1.json similarity index 100% rename from engine/Rules-Azure/6.5.1.json rename to engine/legacy/rules-azure/6.5.1.json diff --git a/engine/Rules-Azure/6.5.2.json b/engine/legacy/rules-azure/6.5.2.json similarity index 100% rename from engine/Rules-Azure/6.5.2.json rename to engine/legacy/rules-azure/6.5.2.json diff --git a/engine/Rules-Azure/6.5.3.json b/engine/legacy/rules-azure/6.5.3.json similarity index 100% rename from engine/Rules-Azure/6.5.3.json rename to engine/legacy/rules-azure/6.5.3.json diff --git a/engine/Rules-Azure/6.5.4.json b/engine/legacy/rules-azure/6.5.4.json similarity index 100% rename from engine/Rules-Azure/6.5.4.json rename to engine/legacy/rules-azure/6.5.4.json diff --git a/engine/Rules-Azure/7.2.1.json b/engine/legacy/rules-azure/7.2.1.json similarity index 100% rename from engine/Rules-Azure/7.2.1.json rename to engine/legacy/rules-azure/7.2.1.json diff --git a/engine/Rules-Azure/7.2.10.json b/engine/legacy/rules-azure/7.2.10.json similarity index 100% rename from engine/Rules-Azure/7.2.10.json rename to engine/legacy/rules-azure/7.2.10.json diff --git a/engine/Rules-Azure/7.2.11.json b/engine/legacy/rules-azure/7.2.11.json similarity index 100% rename from engine/Rules-Azure/7.2.11.json rename to engine/legacy/rules-azure/7.2.11.json diff --git a/engine/Rules-Azure/7.2.2.json b/engine/legacy/rules-azure/7.2.2.json similarity index 100% rename from engine/Rules-Azure/7.2.2.json rename to engine/legacy/rules-azure/7.2.2.json diff --git a/engine/Rules-Azure/7.2.3.json b/engine/legacy/rules-azure/7.2.3.json similarity index 100% rename from engine/Rules-Azure/7.2.3.json rename to engine/legacy/rules-azure/7.2.3.json diff --git a/engine/Rules-Azure/7.2.4.json b/engine/legacy/rules-azure/7.2.4.json similarity index 100% rename from engine/Rules-Azure/7.2.4.json rename to engine/legacy/rules-azure/7.2.4.json diff --git a/engine/Rules-Azure/7.2.5.json b/engine/legacy/rules-azure/7.2.5.json similarity index 100% rename from engine/Rules-Azure/7.2.5.json rename to engine/legacy/rules-azure/7.2.5.json diff --git a/engine/Rules-Azure/7.2.6.json b/engine/legacy/rules-azure/7.2.6.json similarity index 100% rename from engine/Rules-Azure/7.2.6.json rename to engine/legacy/rules-azure/7.2.6.json diff --git a/engine/Rules-Azure/7.2.7.json b/engine/legacy/rules-azure/7.2.7.json similarity index 100% rename from engine/Rules-Azure/7.2.7.json rename to engine/legacy/rules-azure/7.2.7.json diff --git a/engine/Rules-Azure/7.2.8.json b/engine/legacy/rules-azure/7.2.8.json similarity index 100% rename from engine/Rules-Azure/7.2.8.json rename to engine/legacy/rules-azure/7.2.8.json diff --git a/engine/Rules-Azure/7.2.9.json b/engine/legacy/rules-azure/7.2.9.json similarity index 100% rename from engine/Rules-Azure/7.2.9.json rename to engine/legacy/rules-azure/7.2.9.json diff --git a/engine/Rules-Azure/7.3.1.json b/engine/legacy/rules-azure/7.3.1.json similarity index 100% rename from engine/Rules-Azure/7.3.1.json rename to engine/legacy/rules-azure/7.3.1.json diff --git a/engine/Rules-Azure/7.3.2.json b/engine/legacy/rules-azure/7.3.2.json similarity index 100% rename from engine/Rules-Azure/7.3.2.json rename to engine/legacy/rules-azure/7.3.2.json diff --git a/engine/Rules-Azure/7.3.3.json b/engine/legacy/rules-azure/7.3.3.json similarity index 100% rename from engine/Rules-Azure/7.3.3.json rename to engine/legacy/rules-azure/7.3.3.json diff --git a/engine/Rules-Azure/7.3.4.json b/engine/legacy/rules-azure/7.3.4.json similarity index 100% rename from engine/Rules-Azure/7.3.4.json rename to engine/legacy/rules-azure/7.3.4.json diff --git a/engine/Rules-Azure/8.1.1.json b/engine/legacy/rules-azure/8.1.1.json similarity index 100% rename from engine/Rules-Azure/8.1.1.json rename to engine/legacy/rules-azure/8.1.1.json diff --git a/engine/Rules-Azure/8.1.2.json b/engine/legacy/rules-azure/8.1.2.json similarity index 100% rename from engine/Rules-Azure/8.1.2.json rename to engine/legacy/rules-azure/8.1.2.json diff --git a/engine/Rules-Azure/8.2.1.json b/engine/legacy/rules-azure/8.2.1.json similarity index 100% rename from engine/Rules-Azure/8.2.1.json rename to engine/legacy/rules-azure/8.2.1.json diff --git a/engine/Rules-Azure/8.2.2.json b/engine/legacy/rules-azure/8.2.2.json similarity index 100% rename from engine/Rules-Azure/8.2.2.json rename to engine/legacy/rules-azure/8.2.2.json diff --git a/engine/Rules-Azure/8.2.3.json b/engine/legacy/rules-azure/8.2.3.json similarity index 100% rename from engine/Rules-Azure/8.2.3.json rename to engine/legacy/rules-azure/8.2.3.json diff --git a/engine/Rules-Azure/8.2.4.json b/engine/legacy/rules-azure/8.2.4.json similarity index 100% rename from engine/Rules-Azure/8.2.4.json rename to engine/legacy/rules-azure/8.2.4.json diff --git a/engine/Rules-Azure/8.4.1.json b/engine/legacy/rules-azure/8.4.1.json similarity index 100% rename from engine/Rules-Azure/8.4.1.json rename to engine/legacy/rules-azure/8.4.1.json diff --git a/engine/Rules-Azure/8.5.1.json b/engine/legacy/rules-azure/8.5.1.json similarity index 100% rename from engine/Rules-Azure/8.5.1.json rename to engine/legacy/rules-azure/8.5.1.json diff --git a/engine/Rules-Azure/8.5.2.json b/engine/legacy/rules-azure/8.5.2.json similarity index 100% rename from engine/Rules-Azure/8.5.2.json rename to engine/legacy/rules-azure/8.5.2.json diff --git a/engine/Rules-Azure/8.5.3.json b/engine/legacy/rules-azure/8.5.3.json similarity index 100% rename from engine/Rules-Azure/8.5.3.json rename to engine/legacy/rules-azure/8.5.3.json diff --git a/engine/Rules-Azure/8.5.4.json b/engine/legacy/rules-azure/8.5.4.json similarity index 100% rename from engine/Rules-Azure/8.5.4.json rename to engine/legacy/rules-azure/8.5.4.json diff --git a/engine/Rules-Azure/8.5.5.json b/engine/legacy/rules-azure/8.5.5.json similarity index 100% rename from engine/Rules-Azure/8.5.5.json rename to engine/legacy/rules-azure/8.5.5.json diff --git a/engine/Rules-Azure/8.5.6.json b/engine/legacy/rules-azure/8.5.6.json similarity index 100% rename from engine/Rules-Azure/8.5.6.json rename to engine/legacy/rules-azure/8.5.6.json diff --git a/engine/Rules-Azure/8.5.7.json b/engine/legacy/rules-azure/8.5.7.json similarity index 100% rename from engine/Rules-Azure/8.5.7.json rename to engine/legacy/rules-azure/8.5.7.json diff --git a/engine/Rules-Azure/8.5.8.json b/engine/legacy/rules-azure/8.5.8.json similarity index 100% rename from engine/Rules-Azure/8.5.8.json rename to engine/legacy/rules-azure/8.5.8.json diff --git a/engine/Rules-Azure/8.5.9.json b/engine/legacy/rules-azure/8.5.9.json similarity index 100% rename from engine/Rules-Azure/8.5.9.json rename to engine/legacy/rules-azure/8.5.9.json diff --git a/engine/Rules-Azure/8.6.1.json b/engine/legacy/rules-azure/8.6.1.json similarity index 100% rename from engine/Rules-Azure/8.6.1.json rename to engine/legacy/rules-azure/8.6.1.json diff --git a/engine/Rules-Azure/9.1.1.json b/engine/legacy/rules-azure/9.1.1.json similarity index 100% rename from engine/Rules-Azure/9.1.1.json rename to engine/legacy/rules-azure/9.1.1.json diff --git a/engine/Rules-Azure/9.1.10.json b/engine/legacy/rules-azure/9.1.10.json similarity index 100% rename from engine/Rules-Azure/9.1.10.json rename to engine/legacy/rules-azure/9.1.10.json diff --git a/engine/Rules-Azure/9.1.11.json b/engine/legacy/rules-azure/9.1.11.json similarity index 100% rename from engine/Rules-Azure/9.1.11.json rename to engine/legacy/rules-azure/9.1.11.json diff --git a/engine/Rules-Azure/9.1.2.json b/engine/legacy/rules-azure/9.1.2.json similarity index 100% rename from engine/Rules-Azure/9.1.2.json rename to engine/legacy/rules-azure/9.1.2.json diff --git a/engine/Rules-Azure/9.1.3.json b/engine/legacy/rules-azure/9.1.3.json similarity index 100% rename from engine/Rules-Azure/9.1.3.json rename to engine/legacy/rules-azure/9.1.3.json diff --git a/engine/Rules-Azure/9.1.4.json b/engine/legacy/rules-azure/9.1.4.json similarity index 100% rename from engine/Rules-Azure/9.1.4.json rename to engine/legacy/rules-azure/9.1.4.json diff --git a/engine/Rules-Azure/9.1.5.json b/engine/legacy/rules-azure/9.1.5.json similarity index 100% rename from engine/Rules-Azure/9.1.5.json rename to engine/legacy/rules-azure/9.1.5.json diff --git a/engine/Rules-Azure/9.1.6.json b/engine/legacy/rules-azure/9.1.6.json similarity index 100% rename from engine/Rules-Azure/9.1.6.json rename to engine/legacy/rules-azure/9.1.6.json diff --git a/engine/Rules-Azure/9.1.7.json b/engine/legacy/rules-azure/9.1.7.json similarity index 100% rename from engine/Rules-Azure/9.1.7.json rename to engine/legacy/rules-azure/9.1.7.json diff --git a/engine/Rules-Azure/9.1.8.json b/engine/legacy/rules-azure/9.1.8.json similarity index 100% rename from engine/Rules-Azure/9.1.8.json rename to engine/legacy/rules-azure/9.1.8.json diff --git a/engine/Rules-Azure/9.1.9.json b/engine/legacy/rules-azure/9.1.9.json similarity index 100% rename from engine/Rules-Azure/9.1.9.json rename to engine/legacy/rules-azure/9.1.9.json diff --git a/engine/Rules-Azure/main.py b/engine/legacy/rules-azure/main.py similarity index 76% rename from engine/Rules-Azure/main.py rename to engine/legacy/rules-azure/main.py index 7521580d..e6ffea07 100644 --- a/engine/Rules-Azure/main.py +++ b/engine/legacy/rules-azure/main.py @@ -1,5 +1,14 @@ import json import os +import sys +from pathlib import Path + +# Allow this legacy script to be run from repo root (or directly) by ensuring +# the legacy engine helpers are importable. +_LEGACY_ENGINE_DIR = Path(__file__).resolve().parents[1] / "engine" +if str(_LEGACY_ENGINE_DIR) not in sys.path: + sys.path.insert(0, str(_LEGACY_ENGINE_DIR)) + from rule_helpers import match_value, match_in_list, match_regex, match_range from risk_rating import calculate_impact_level, calculate_risk_level @@ -27,16 +36,19 @@ def evaluate_rule(rule, config): reason = f"{rule['tags']} = {value}, expected {expected} | Severity: {severity}" return False, reason, severity -def load_mock_config(path="engine/test-configs/iam_policy.json"): - with open(path) as f: +def load_mock_config(path: str | os.PathLike | None = None): + legacy_root = Path(__file__).resolve().parents[1] + config_path = Path(path) if path else (legacy_root / "test-configs" / "iam_policy.json") + with open(config_path) as f: return json.load(f) -def load_rules(directory="engine/rules"): +def load_rules(directory: str | os.PathLike | None = None): + rules_dir = Path(directory) if directory else Path(__file__).resolve().parent rules = [] - for file in os.listdir(directory): + for file in os.listdir(rules_dir): if file.endswith(".json"): - with open(os.path.join(directory, file)) as f: + with open(os.path.join(rules_dir, file)) as f: rule = json.load(f) rules.append(rule) return rules diff --git a/engine/rules/CIS_GCP_1_5.rego b/engine/legacy/rules-gcp/CIS_GCP_1_5.rego similarity index 100% rename from engine/rules/CIS_GCP_1_5.rego rename to engine/legacy/rules-gcp/CIS_GCP_1_5.rego diff --git a/engine/rules/CIS_GCP_1_6.rego b/engine/legacy/rules-gcp/CIS_GCP_1_6.rego similarity index 100% rename from engine/rules/CIS_GCP_1_6.rego rename to engine/legacy/rules-gcp/CIS_GCP_1_6.rego diff --git a/engine/rules/CIS_GCP_1_8.rego b/engine/legacy/rules-gcp/CIS_GCP_1_8.rego similarity index 100% rename from engine/rules/CIS_GCP_1_8.rego rename to engine/legacy/rules-gcp/CIS_GCP_1_8.rego diff --git a/engine/rules/CIS_GCP_2_1.rego b/engine/legacy/rules-gcp/CIS_GCP_2_1.rego similarity index 100% rename from engine/rules/CIS_GCP_2_1.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_1.rego diff --git a/engine/rules/CIS_GCP_2_10.rego b/engine/legacy/rules-gcp/CIS_GCP_2_10.rego similarity index 100% rename from engine/rules/CIS_GCP_2_10.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_10.rego diff --git a/engine/rules/CIS_GCP_2_11.rego b/engine/legacy/rules-gcp/CIS_GCP_2_11.rego similarity index 100% rename from engine/rules/CIS_GCP_2_11.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_11.rego diff --git a/engine/rules/CIS_GCP_2_12.rego b/engine/legacy/rules-gcp/CIS_GCP_2_12.rego similarity index 100% rename from engine/rules/CIS_GCP_2_12.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_12.rego diff --git a/engine/rules/CIS_GCP_2_13.rego b/engine/legacy/rules-gcp/CIS_GCP_2_13.rego similarity index 100% rename from engine/rules/CIS_GCP_2_13.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_13.rego diff --git a/engine/rules/CIS_GCP_2_15.rego b/engine/legacy/rules-gcp/CIS_GCP_2_15.rego similarity index 100% rename from engine/rules/CIS_GCP_2_15.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_15.rego diff --git a/engine/rules/CIS_GCP_2_16.rego b/engine/legacy/rules-gcp/CIS_GCP_2_16.rego similarity index 100% rename from engine/rules/CIS_GCP_2_16.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_16.rego diff --git a/engine/rules/CIS_GCP_2_2.rego b/engine/legacy/rules-gcp/CIS_GCP_2_2.rego similarity index 100% rename from engine/rules/CIS_GCP_2_2.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_2.rego diff --git a/engine/rules/CIS_GCP_2_3.rego b/engine/legacy/rules-gcp/CIS_GCP_2_3.rego similarity index 100% rename from engine/rules/CIS_GCP_2_3.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_3.rego diff --git a/engine/rules/CIS_GCP_2_4.rego b/engine/legacy/rules-gcp/CIS_GCP_2_4.rego similarity index 100% rename from engine/rules/CIS_GCP_2_4.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_4.rego diff --git a/engine/rules/CIS_GCP_2_5.rego b/engine/legacy/rules-gcp/CIS_GCP_2_5.rego similarity index 100% rename from engine/rules/CIS_GCP_2_5.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_5.rego diff --git a/engine/rules/CIS_GCP_2_6.rego b/engine/legacy/rules-gcp/CIS_GCP_2_6.rego similarity index 100% rename from engine/rules/CIS_GCP_2_6.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_6.rego diff --git a/engine/rules/CIS_GCP_2_7.rego b/engine/legacy/rules-gcp/CIS_GCP_2_7.rego similarity index 100% rename from engine/rules/CIS_GCP_2_7.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_7.rego diff --git a/engine/rules/CIS_GCP_2_8.rego b/engine/legacy/rules-gcp/CIS_GCP_2_8.rego similarity index 100% rename from engine/rules/CIS_GCP_2_8.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_8.rego diff --git a/engine/rules/CIS_GCP_2_9.rego b/engine/legacy/rules-gcp/CIS_GCP_2_9.rego similarity index 100% rename from engine/rules/CIS_GCP_2_9.rego rename to engine/legacy/rules-gcp/CIS_GCP_2_9.rego diff --git a/engine/rules/CIS_GCP_3_1.rego b/engine/legacy/rules-gcp/CIS_GCP_3_1.rego similarity index 100% rename from engine/rules/CIS_GCP_3_1.rego rename to engine/legacy/rules-gcp/CIS_GCP_3_1.rego diff --git a/engine/rules/CIS_GCP_3_2.rego b/engine/legacy/rules-gcp/CIS_GCP_3_2.rego similarity index 100% rename from engine/rules/CIS_GCP_3_2.rego rename to engine/legacy/rules-gcp/CIS_GCP_3_2.rego diff --git a/engine/rules/CIS_GCP_3_3.rego b/engine/legacy/rules-gcp/CIS_GCP_3_3.rego similarity index 100% rename from engine/rules/CIS_GCP_3_3.rego rename to engine/legacy/rules-gcp/CIS_GCP_3_3.rego diff --git a/engine/rules/CIS_GCP_3_6.rego b/engine/legacy/rules-gcp/CIS_GCP_3_6.rego similarity index 100% rename from engine/rules/CIS_GCP_3_6.rego rename to engine/legacy/rules-gcp/CIS_GCP_3_6.rego diff --git a/engine/rules/CIS_GCP_3_7.rego b/engine/legacy/rules-gcp/CIS_GCP_3_7.rego similarity index 100% rename from engine/rules/CIS_GCP_3_7.rego rename to engine/legacy/rules-gcp/CIS_GCP_3_7.rego diff --git a/engine/rules/CIS_GCP_4_1.rego b/engine/legacy/rules-gcp/CIS_GCP_4_1.rego similarity index 100% rename from engine/rules/CIS_GCP_4_1.rego rename to engine/legacy/rules-gcp/CIS_GCP_4_1.rego diff --git a/engine/rules/CIS_GCP_4_11.rego b/engine/legacy/rules-gcp/CIS_GCP_4_11.rego similarity index 100% rename from engine/rules/CIS_GCP_4_11.rego rename to engine/legacy/rules-gcp/CIS_GCP_4_11.rego diff --git a/engine/rules/CIS_GCP_4_2.rego b/engine/legacy/rules-gcp/CIS_GCP_4_2.rego similarity index 100% rename from engine/rules/CIS_GCP_4_2.rego rename to engine/legacy/rules-gcp/CIS_GCP_4_2.rego diff --git a/engine/rules/CIS_GCP_4_3.rego b/engine/legacy/rules-gcp/CIS_GCP_4_3.rego similarity index 100% rename from engine/rules/CIS_GCP_4_3.rego rename to engine/legacy/rules-gcp/CIS_GCP_4_3.rego diff --git a/engine/rules/CIS_GCP_4_4.rego b/engine/legacy/rules-gcp/CIS_GCP_4_4.rego similarity index 100% rename from engine/rules/CIS_GCP_4_4.rego rename to engine/legacy/rules-gcp/CIS_GCP_4_4.rego diff --git a/engine/rules/CIS_GCP_4_5.rego b/engine/legacy/rules-gcp/CIS_GCP_4_5.rego similarity index 100% rename from engine/rules/CIS_GCP_4_5.rego rename to engine/legacy/rules-gcp/CIS_GCP_4_5.rego diff --git a/engine/rules/CIS_GCP_4_6.rego b/engine/legacy/rules-gcp/CIS_GCP_4_6.rego similarity index 100% rename from engine/rules/CIS_GCP_4_6.rego rename to engine/legacy/rules-gcp/CIS_GCP_4_6.rego diff --git a/engine/rules/CIS_GCP_4_8.rego b/engine/legacy/rules-gcp/CIS_GCP_4_8.rego similarity index 100% rename from engine/rules/CIS_GCP_4_8.rego rename to engine/legacy/rules-gcp/CIS_GCP_4_8.rego diff --git a/engine/rules/CIS_GCP_4_9.rego b/engine/legacy/rules-gcp/CIS_GCP_4_9.rego similarity index 100% rename from engine/rules/CIS_GCP_4_9.rego rename to engine/legacy/rules-gcp/CIS_GCP_4_9.rego diff --git a/engine/rules/CIS_GCP_5_1.rego b/engine/legacy/rules-gcp/CIS_GCP_5_1.rego similarity index 100% rename from engine/rules/CIS_GCP_5_1.rego rename to engine/legacy/rules-gcp/CIS_GCP_5_1.rego diff --git a/engine/rules/CIS_GCP_5_2.rego b/engine/legacy/rules-gcp/CIS_GCP_5_2.rego similarity index 100% rename from engine/rules/CIS_GCP_5_2.rego rename to engine/legacy/rules-gcp/CIS_GCP_5_2.rego diff --git a/engine/rules/CIS_GCP_6_1_2.rego b/engine/legacy/rules-gcp/CIS_GCP_6_1_2.rego similarity index 100% rename from engine/rules/CIS_GCP_6_1_2.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_1_2.rego diff --git a/engine/rules/CIS_GCP_6_1_3.rego b/engine/legacy/rules-gcp/CIS_GCP_6_1_3.rego similarity index 100% rename from engine/rules/CIS_GCP_6_1_3.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_1_3.rego diff --git a/engine/rules/CIS_GCP_6_2_1.rego b/engine/legacy/rules-gcp/CIS_GCP_6_2_1.rego similarity index 100% rename from engine/rules/CIS_GCP_6_2_1.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_2_1.rego diff --git a/engine/rules/CIS_GCP_6_2_2.rego b/engine/legacy/rules-gcp/CIS_GCP_6_2_2.rego similarity index 100% rename from engine/rules/CIS_GCP_6_2_2.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_2_2.rego diff --git a/engine/rules/CIS_GCP_6_2_3.rego b/engine/legacy/rules-gcp/CIS_GCP_6_2_3.rego similarity index 100% rename from engine/rules/CIS_GCP_6_2_3.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_2_3.rego diff --git a/engine/rules/CIS_GCP_6_2_4.rego b/engine/legacy/rules-gcp/CIS_GCP_6_2_4.rego similarity index 100% rename from engine/rules/CIS_GCP_6_2_4.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_2_4.rego diff --git a/engine/rules/CIS_GCP_6_2_5.rego b/engine/legacy/rules-gcp/CIS_GCP_6_2_5.rego similarity index 100% rename from engine/rules/CIS_GCP_6_2_5.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_2_5.rego diff --git a/engine/rules/CIS_GCP_6_2_6.rego b/engine/legacy/rules-gcp/CIS_GCP_6_2_6.rego similarity index 100% rename from engine/rules/CIS_GCP_6_2_6.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_2_6.rego diff --git a/engine/rules/CIS_GCP_6_2_7.rego b/engine/legacy/rules-gcp/CIS_GCP_6_2_7.rego similarity index 100% rename from engine/rules/CIS_GCP_6_2_7.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_2_7.rego diff --git a/engine/rules/CIS_GCP_6_2_8.rego b/engine/legacy/rules-gcp/CIS_GCP_6_2_8.rego similarity index 100% rename from engine/rules/CIS_GCP_6_2_8.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_2_8.rego diff --git a/engine/rules/CIS_GCP_6_3_1.rego b/engine/legacy/rules-gcp/CIS_GCP_6_3_1.rego similarity index 100% rename from engine/rules/CIS_GCP_6_3_1.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_3_1.rego diff --git a/engine/rules/CIS_GCP_6_3_2.rego b/engine/legacy/rules-gcp/CIS_GCP_6_3_2.rego similarity index 100% rename from engine/rules/CIS_GCP_6_3_2.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_3_2.rego diff --git a/engine/rules/CIS_GCP_6_3_3.rego b/engine/legacy/rules-gcp/CIS_GCP_6_3_3.rego similarity index 100% rename from engine/rules/CIS_GCP_6_3_3.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_3_3.rego diff --git a/engine/rules/CIS_GCP_6_3_4.rego b/engine/legacy/rules-gcp/CIS_GCP_6_3_4.rego similarity index 100% rename from engine/rules/CIS_GCP_6_3_4.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_3_4.rego diff --git a/engine/rules/CIS_GCP_6_3_5.rego b/engine/legacy/rules-gcp/CIS_GCP_6_3_5.rego similarity index 100% rename from engine/rules/CIS_GCP_6_3_5.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_3_5.rego diff --git a/engine/rules/CIS_GCP_6_3_6.rego b/engine/legacy/rules-gcp/CIS_GCP_6_3_6.rego similarity index 100% rename from engine/rules/CIS_GCP_6_3_6.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_3_6.rego diff --git a/engine/rules/CIS_GCP_6_3_7.rego b/engine/legacy/rules-gcp/CIS_GCP_6_3_7.rego similarity index 100% rename from engine/rules/CIS_GCP_6_3_7.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_3_7.rego diff --git a/engine/rules/CIS_GCP_6_4.rego b/engine/legacy/rules-gcp/CIS_GCP_6_4.rego similarity index 100% rename from engine/rules/CIS_GCP_6_4.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_4.rego diff --git a/engine/rules/CIS_GCP_6_5.rego b/engine/legacy/rules-gcp/CIS_GCP_6_5.rego similarity index 100% rename from engine/rules/CIS_GCP_6_5.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_5.rego diff --git a/engine/rules/CIS_GCP_6_6.rego b/engine/legacy/rules-gcp/CIS_GCP_6_6.rego similarity index 100% rename from engine/rules/CIS_GCP_6_6.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_6.rego diff --git a/engine/rules/CIS_GCP_6_7.rego b/engine/legacy/rules-gcp/CIS_GCP_6_7.rego similarity index 100% rename from engine/rules/CIS_GCP_6_7.rego rename to engine/legacy/rules-gcp/CIS_GCP_6_7.rego diff --git a/engine/rules/CIS_GCP_7_1.rego b/engine/legacy/rules-gcp/CIS_GCP_7_1.rego similarity index 100% rename from engine/rules/CIS_GCP_7_1.rego rename to engine/legacy/rules-gcp/CIS_GCP_7_1.rego diff --git a/engine/rules/CIS_GCP_7_2.rego b/engine/legacy/rules-gcp/CIS_GCP_7_2.rego similarity index 100% rename from engine/rules/CIS_GCP_7_2.rego rename to engine/legacy/rules-gcp/CIS_GCP_7_2.rego diff --git a/engine/rules/CIS_GCP_7_3.rego b/engine/legacy/rules-gcp/CIS_GCP_7_3.rego similarity index 100% rename from engine/rules/CIS_GCP_7_3.rego rename to engine/legacy/rules-gcp/CIS_GCP_7_3.rego diff --git a/engine/rules/CIS_GCP_8_1.rego b/engine/legacy/rules-gcp/CIS_GCP_8_1.rego similarity index 100% rename from engine/rules/CIS_GCP_8_1.rego rename to engine/legacy/rules-gcp/CIS_GCP_8_1.rego diff --git a/engine/Template/Template_Policy.rego b/engine/legacy/template/Template_Policy.rego similarity index 100% rename from engine/Template/Template_Policy.rego rename to engine/legacy/template/Template_Policy.rego diff --git a/engine/test-configs/ComputeInstances.json b/engine/legacy/test-configs/ComputeInstances.json similarity index 100% rename from engine/test-configs/ComputeInstances.json rename to engine/legacy/test-configs/ComputeInstances.json diff --git a/engine/test-configs/bigquery_datasets_full.json b/engine/legacy/test-configs/bigquery_datasets_full.json similarity index 100% rename from engine/test-configs/bigquery_datasets_full.json rename to engine/legacy/test-configs/bigquery_datasets_full.json diff --git a/engine/test-configs/bucket_iam_policies.json b/engine/legacy/test-configs/bucket_iam_policies.json similarity index 100% rename from engine/test-configs/bucket_iam_policies.json rename to engine/legacy/test-configs/bucket_iam_policies.json diff --git a/engine/test-configs/dataproc_clusters.json b/engine/legacy/test-configs/dataproc_clusters.json similarity index 100% rename from engine/test-configs/dataproc_clusters.json rename to engine/legacy/test-configs/dataproc_clusters.json diff --git a/engine/test-configs/dns_zones.json b/engine/legacy/test-configs/dns_zones.json similarity index 100% rename from engine/test-configs/dns_zones.json rename to engine/legacy/test-configs/dns_zones.json diff --git a/engine/test-configs/firewalls.json b/engine/legacy/test-configs/firewalls.json similarity index 100% rename from engine/test-configs/firewalls.json rename to engine/legacy/test-configs/firewalls.json diff --git a/engine/test-configs/iam_policy.json b/engine/legacy/test-configs/iam_policy.json similarity index 100% rename from engine/test-configs/iam_policy.json rename to engine/legacy/test-configs/iam_policy.json diff --git a/engine/test-configs/networks.json b/engine/legacy/test-configs/networks.json similarity index 100% rename from engine/test-configs/networks.json rename to engine/legacy/test-configs/networks.json diff --git a/engine/test-configs/sql_instances.json b/engine/legacy/test-configs/sql_instances.json similarity index 100% rename from engine/test-configs/sql_instances.json rename to engine/legacy/test-configs/sql_instances.json diff --git a/engine/opa_client.py b/engine/opa_client.py new file mode 100644 index 00000000..7d345752 --- /dev/null +++ b/engine/opa_client.py @@ -0,0 +1,67 @@ +"""OPA (Open Policy Agent) client for policy evaluation.""" + +import httpx + +from worker.config import settings + + +class OPAClient: + """Client for querying Open Policy Agent for policy evaluation.""" + + def __init__(self, base_url: str | None = None): + """Initialize OPA client. + + Args: + base_url: OPA server URL. Defaults to OPA_URL from settings. + """ + self.base_url = (base_url or settings.OPA_URL).rstrip("/") + + async def evaluate_policy( + self, package_path: str, input_data: dict + ) -> dict: + """Evaluate a policy against input data. + + Args: + package_path: The OPA package path (e.g., "cis/microsoft_365_foundations/v3_1_0/control_1_1_1") + input_data: The data to evaluate (facts collected from the cloud) + + Returns: + The evaluation result from OPA, typically containing: + - compliant: bool + - message: str + - affected_resources: list + - details: dict + """ + # Convert package path to OPA URL format + # e.g., "cis.microsoft_365_foundations.v3_1_0.control_1_1_1" -> "cis/microsoft_365_foundations/v3_1_0/control_1_1_1" + url_path = package_path.replace(".", "/") + + # Query the specific "result" rule within the package + # This gives us the compliance result directly without extra nesting + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + f"{self.base_url}/v1/data/{url_path}/result", + json={"input": input_data}, + ) + response.raise_for_status() + result = response.json() + + # OPA returns {"result": {...}} - extract the result + return result.get("result", {}) + + async def health_check(self) -> bool: + """Check if OPA server is healthy. + + Returns: + True if OPA is responding, False otherwise. + """ + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(f"{self.base_url}/health") + return response.status_code == 200 + except Exception: + return False + + +# Default client instance +opa_client = OPAClient() diff --git a/engine/policies/README.md b/engine/policies/README.md new file mode 100644 index 00000000..5f5b7eef --- /dev/null +++ b/engine/policies/README.md @@ -0,0 +1,115 @@ +# Policy Metadata in OPA + +This directory contains compliance policies written in Rego for evaluation by Open Policy Agent (OPA). We use OPA's native metadata annotations to embed control information directly in the policy files themselves. + +## Why embed metadata in the policy files? + +We considered a few approaches for managing policy metadata: + +1. **Separate JSON/YAML files** - Store metadata in sidecar files alongside policies +2. **Database storage** - Keep metadata in our application database +3. **OPA annotations** - Embed metadata directly in Rego files using the `# METADATA` block + +We went with option 3 because it keeps everything in one place. When you're reading a policy, you immediately see what control it implements, what permissions it needs, and how to fix issues it finds. No jumping between files or querying a database. + +OPA's annotation system was designed for exactly this use case. The metadata travels with the policy through version control, code review, and deployment. If someone copies a policy file to another system, the context goes with it. + +## The annotation format + +OPA expects metadata in a YAML block at the top of the file, starting with `# METADATA`. Here's the structure we use: + +```rego +# METADATA +# title: Short name of the control +# description: | +# What this control checks for and why it matters. +# Can span multiple lines using YAML's pipe syntax. +# related_resources: +# - ref: https://example.com/benchmark +# description: Link to the source benchmark +# custom: +# control_id: CIS-1.1.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v3.1.0 +# severity: critical +# service: EntraID +# requires_permissions: +# - User.Read.All + +package your.package.name + +# ... policy rules ... +``` + +### Standard OPA fields + +These are recognized by OPA tooling: + +- **title** - A short, human-readable name +- **description** - What the policy does +- **related_resources** - Links to external documentation (benchmark sources, etc.) + +### Custom fields + +Everything under `custom:` is ours to define. We use: + +- **control_id** - The identifier from the compliance framework (e.g., CIS-1.1.1) +- **framework** - Which framework this comes from (cis, nist, essential8, etc.) +- **benchmark** - The specific benchmark slug +- **version** - Benchmark version +- **severity** - Risk level: critical, high, medium, or low +- **service** - The cloud service being checked (EntraID, Exchange, SharePoint, etc.) +- **requires_permissions** - API permissions needed to collect data for this control + + +## Accessing metadata at runtime + +OPA provides built-in functions to query metadata: + +```rego +# Get metadata for the current rule +metadata := rego.metadata.rule() + +# Get the full annotation chain (including package-level metadata) +chain := rego.metadata.chain() +``` + +You can also inspect all metadata from the command line: + +```bash +opa inspect -a policies/ +``` + +This is useful for building tooling that generates documentation or validates that all policies have required fields. + +## Directory structure + +Policies are organized by framework, benchmark, and version: + +``` +policies/ + cis/ + microsoft-365-foundations/ + v3.1.0/ + 1.1.1_admin_cloud_only.rego + 1.1.3_global_admin_count.rego + gcp/ + v1.0.0/ + ... + essential8/ + ... +``` + +The package name in each policy mirrors this structure, with dots replacing slashes and hyphens becoming underscores: + +``` +cis/microsoft-365-foundations/v3.1.0/1.1.1_... +→ package cis.microsoft_365_foundations.v3_1_0.control_1_1_1 +``` + +## References + +- [OPA Policy Language - Annotations](https://www.openpolicyagent.org/docs/latest/policy-language/#annotations) +- [OPA Policy Reference - Metadata Built-ins](https://www.openpolicyagent.org/docs/latest/policy-reference/#rego) +- [Styra Rego Style Guide](https://docs.styra.com/opa/rego-style-guide) diff --git a/engine/policies/cis/microsoft-365-foundations/v3.1.0/1.1.1_admin_cloud_only.rego b/engine/policies/cis/microsoft-365-foundations/v3.1.0/1.1.1_admin_cloud_only.rego new file mode 100644 index 00000000..094f41e8 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v3.1.0/1.1.1_admin_cloud_only.rego @@ -0,0 +1,47 @@ +# METADATA +# title: Ensure Administrative accounts are cloud-only +# description: | +# Administrative accounts should not be synced from on-premises Active Directory. +# Cloud-only accounts reduce the attack surface by not being tied to on-premises +# infrastructure, preventing lateral movement from compromised on-prem environments. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-1.1.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v3.1.0 +# severity: critical +# service: EntraID +# requires_permissions: +# - User.Read.All +# - RoleManagement.Read.Directory + +package cis.microsoft_365_foundations.v3_1_0.control_1_1_1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + synced_admins := [a | some a in input.admin_accounts; a.on_premises_sync_enabled == true] + output := { + "compliant": count(synced_admins) == 0, + "message": generate_message(synced_admins, input.admin_accounts), + "affected_resources": [a.userPrincipalName | some a in synced_admins], + "details": { + "total_admin_accounts": count(input.admin_accounts), + "synced_admin_count": count(synced_admins), + "cloud_only_admin_count": count(input.admin_accounts) - count(synced_admins) + } + } +} + +generate_message(synced_admins, all_admins) := msg if { + count(synced_admins) == 0 + msg := sprintf("All %d administrative accounts are cloud-only", [count(all_admins)]) +} + +generate_message(synced_admins, all_admins) := msg if { + count(synced_admins) > 0 + msg := sprintf("%d of %d administrative accounts are synced from on-premises AD", [count(synced_admins), count(all_admins)]) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v3.1.0/1.1.3_global_admin_count.rego b/engine/policies/cis/microsoft-365-foundations/v3.1.0/1.1.3_global_admin_count.rego new file mode 100644 index 00000000..d82216a4 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v3.1.0/1.1.3_global_admin_count.rego @@ -0,0 +1,61 @@ +# METADATA +# title: Ensure that between two and four global admins are designated +# description: | +# Maintain between 2-4 global administrators to ensure operational continuity +# while minimizing attack surface. Having fewer than 2 creates a single point +# of failure, while having more than 4 unnecessarily expands the attack surface +# and increases the risk of credential compromise. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-1.1.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v3.1.0 +# severity: high +# service: EntraID +# requires_permissions: +# - RoleManagement.Read.Directory +# - User.Read.All + +package cis.microsoft_365_foundations.v3_1_0.control_1_1_3 + +default result := {"compliant": false, "message": "Evaluation failed"} + +default is_compliant := false + +is_compliant if { + input.global_admin_count >= 2 + input.global_admin_count <= 4 +} + +result := output if { + admin_count := input.global_admin_count + output := { + "compliant": is_compliant, + "message": generate_message(admin_count), + "affected_resources": input.global_admins, + "details": { + "global_admin_count": admin_count, + "recommended_min": 2, + "recommended_max": 4 + } + } +} + +generate_message(admin_count) := msg if { + admin_count < 2 + msg := sprintf("Only %d global admin(s) found. Minimum 2 recommended for continuity.", [admin_count]) +} + +generate_message(admin_count) := msg if { + admin_count > 4 + msg := sprintf("%d global admins found. Maximum 4 recommended to minimize attack surface.", [admin_count]) +} + +generate_message(admin_count) := msg if { + admin_count >= 2 + admin_count <= 4 + msg := sprintf("%d global admins configured (within recommended range of 2-4)", [admin_count]) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v3.1.0/metadata.json b/engine/policies/cis/microsoft-365-foundations/v3.1.0/metadata.json new file mode 100644 index 00000000..5e182ffb --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v3.1.0/metadata.json @@ -0,0 +1,41 @@ +{ + "framework": "cis", + "benchmark": "CIS Microsoft 365 Foundations", + "slug": "microsoft-365-foundations", + "version": "v3.1.0", + "release_date": "2024-04-29", + "platform": "m365", + "source_url": "https://www.cisecurity.org/benchmark/microsoft_365", + "controls": [ + { + "control_id": "1.1.1", + "title": "Ensure Administrative accounts are cloud-only", + "description": "Administrative accounts should not be synced from on-premises Active Directory. Cloud-only accounts reduce the attack surface by not being tied to on-premises infrastructure.", + "severity": "critical", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.roles.cloud_only_admins", + "policy_file": "1.1.1_admin_cloud_only.rego", + "requires_permissions": ["User.Read.All", "RoleManagement.Read.Directory"], + "notes": null + }, + { + "control_id": "1.1.3", + "title": "Ensure that between two and four global admins are designated", + "description": "Maintain between 2-4 global administrators to ensure operational continuity while minimizing attack surface. Having fewer than 2 creates a single point of failure, while having more than 4 unnecessarily expands the attack surface.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.roles.privileged_roles", + "policy_file": "1.1.3_global_admin_count.rego", + "requires_permissions": ["RoleManagement.Read.Directory", "User.Read.All"], + "notes": null + } + ] +} diff --git a/engine/policies/cis/microsoft-365-foundations/v4.0.0/1.3.1_password_expiration.rego b/engine/policies/cis/microsoft-365-foundations/v4.0.0/1.3.1_password_expiration.rego new file mode 100644 index 00000000..4b738ab1 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v4.0.0/1.3.1_password_expiration.rego @@ -0,0 +1,52 @@ +# METADATA +# title: Ensure password expiration policy is set to never expire +# description: | +# Configure password policies to never expire. NIST and Microsoft recommend +# this approach when MFA is enabled, as forced password rotation leads to +# weaker passwords without providing security benefits. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-1.3.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v4.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Domain.Read.All + +package cis.microsoft_365_foundations.v4_0_0.control_1_3_1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +# 2147483647 = "never expires" (max int32) +never_expires := 2147483647 + +result := output if { + managed_domains := [d | some d in input.domains; d.is_managed == true] + non_compliant := [d | some d in managed_domains; d.password_validity_days != never_expires] + + output := { + "compliant": count(non_compliant) == 0, + "message": generate_message(managed_domains, non_compliant), + "affected_resources": [d.domain_name | some d in non_compliant], + "details": { + "total_managed_domains": count(managed_domains), + "compliant_domains": count(managed_domains) - count(non_compliant), + "non_compliant_domains": count(non_compliant), + "domains": [{"name": d.domain_name, "password_validity_days": d.password_validity_days} | some d in managed_domains] + } + } +} + +generate_message(managed_domains, non_compliant) := msg if { + count(non_compliant) == 0 + msg := sprintf("All %d managed domain(s) have password expiration disabled", [count(managed_domains)]) +} + +generate_message(managed_domains, non_compliant) := msg if { + count(non_compliant) > 0 + msg := sprintf("%d of %d managed domain(s) have password expiration enabled", [count(non_compliant), count(managed_domains)]) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v4.0.0/5.2.2.3_block_legacy_auth.rego b/engine/policies/cis/microsoft-365-foundations/v4.0.0/5.2.2.3_block_legacy_auth.rego new file mode 100644 index 00000000..05be0c1b --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v4.0.0/5.2.2.3_block_legacy_auth.rego @@ -0,0 +1,60 @@ +# METADATA +# title: Ensure Conditional Access policies block legacy authentication +# description: | +# Legacy authentication protocols (IMAP, SMTP, POP3, older Office clients) +# do not support MFA and are commonly exploited in password spray and +# credential stuffing attacks. Block these protocols via Conditional Access. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-5.2.2.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v4.0.0 +# severity: high +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v4_0_0.control_5_2_2_3 + +default result := {"compliant": false, "message": "Evaluation failed"} + +# Check if a policy blocks legacy authentication for all users/apps +is_legacy_auth_block_policy(policy) if { + policy.state == "enabled" + policy.targets_all_users == true + policy.targets_all_apps == true + policy.blocks_legacy_auth == true + policy.grant_control == "block" +} + +result := output if { + blocking_policies := [p | some p in input.conditional_access_policies; is_legacy_auth_block_policy(p)] + compliant := count(blocking_policies) > 0 + + output := { + "compliant": compliant, + "message": generate_message(blocking_policies, input.conditional_access_policies), + "affected_resources": generate_affected_resources(compliant, blocking_policies), + "details": { + "total_policies": count(input.conditional_access_policies), + "legacy_auth_block_policies": count(blocking_policies), + "blocking_policy_names": [p.display_name | some p in blocking_policies] + } + } +} + +generate_message(blocking_policies, all_policies) := msg if { + count(blocking_policies) > 0 + msg := sprintf("Found %d Conditional Access policy(ies) blocking legacy authentication", [count(blocking_policies)]) +} + +generate_message(blocking_policies, all_policies) := msg if { + count(blocking_policies) == 0 + msg := "No Conditional Access policy found that blocks legacy authentication for all users and applications" +} + +generate_affected_resources(true, _) := [] +generate_affected_resources(false, _) := ["No legacy auth blocking policy configured"] diff --git a/engine/policies/cis/microsoft-365-foundations/v4.0.0/metadata.json b/engine/policies/cis/microsoft-365-foundations/v4.0.0/metadata.json new file mode 100644 index 00000000..69a73881 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v4.0.0/metadata.json @@ -0,0 +1,41 @@ +{ + "framework": "cis", + "benchmark": "CIS Microsoft 365 Foundations", + "slug": "microsoft-365-foundations", + "version": "v4.0.0", + "release_date": "2024-10-31", + "platform": "m365", + "source_url": "https://www.cisecurity.org/benchmark/microsoft_365", + "controls": [ + { + "control_id": "1.3.1", + "title": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire'", + "description": "Configure password policies to never expire. NIST and Microsoft recommend this approach when MFA is enabled, as forced rotation leads to weaker passwords.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.domains.password_policy", + "policy_file": "1.3.1_password_expiration.rego", + "requires_permissions": ["Domain.Read.All"], + "notes": null + }, + { + "control_id": "5.2.2.3", + "title": "Ensure Conditional Access policies are configured to block legacy authentication", + "description": "Legacy authentication protocols do not support MFA and are commonly exploited in attacks. Implement Conditional Access policies to block these protocols.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.conditional_access.legacy_auth_block", + "policy_file": "5.2.2.3_block_legacy_auth.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + } + ] +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.1.1_admin_cloud_only.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.1.1_admin_cloud_only.rego new file mode 100644 index 00000000..6c601d4e --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.1.1_admin_cloud_only.rego @@ -0,0 +1,67 @@ +# METADATA +# title: Ensure Administrative accounts are cloud-only +# description: | +# Administrative accounts should not be synced from on-premises Active Directory. +# Cloud-only accounts reduce the attack surface by not being tied to on-premises +# infrastructure, preventing lateral movement from compromised on-prem environments. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-1.1.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: critical +# service: EntraID +# requires_permissions: +# - User.Read.All +# - RoleManagement.Read.Directory + +package cis.microsoft_365_foundations.v6_0_0.control_1_1_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve administrative account data", + "details": {} +} + +# Main evaluation rule +result := output if { + admin_accounts := get_array(input, "admin_accounts") + synced_admins := [a | some a in admin_accounts; a.on_premises_sync_enabled == true] + + compliant := count(synced_admins) == 0 + + msg := build_message(admin_accounts, synced_admins) + affected := [a.userPrincipalName | some a in synced_admins] + + output := { + "compliant": compliant, + "message": msg, + "affected_resources": affected, + "details": { + "total_admin_accounts": count(admin_accounts), + "synced_admin_count": count(synced_admins), + "cloud_only_admin_count": count(admin_accounts) - count(synced_admins), + "synced_admins": [{"userPrincipalName": a.userPrincipalName, "displayName": a.displayName, "admin_roles": a.admin_roles} | some a in synced_admins] + } + } +} + +# Helper to get array with default +get_array(obj, key) := value if { + value := obj[key] +} else := [] + +build_message(all_admins, synced_admins) := msg if { + count(synced_admins) == 0 + msg := sprintf("All %d administrative account(s) are cloud-only", [count(all_admins)]) +} + +build_message(all_admins, synced_admins) := msg if { + count(synced_admins) > 0 + msg := sprintf("%d of %d administrative account(s) are synced from on-premises AD", [count(synced_admins), count(all_admins)]) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.1.3_global_admin_count.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.1.3_global_admin_count.rego new file mode 100644 index 00000000..dd143bdf --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.1.3_global_admin_count.rego @@ -0,0 +1,74 @@ +# METADATA +# title: Ensure that between two and four global admins are designated +# description: | +# Maintain between 2-4 global administrators to ensure operational continuity +# while minimizing attack surface. Having fewer than 2 creates a single point +# of failure, while having more than 4 unnecessarily expands the attack surface +# and increases the risk of credential compromise. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-1.1.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: EntraID +# requires_permissions: +# - RoleManagement.Read.Directory +# - User.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_1_1_3 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve Global Administrator role membership", + "details": {} +} + +default compliant := false + +compliant if { + admin_count := input.global_admin_count + admin_count >= 2 + admin_count <= 4 +} + +result := output if { + admin_count := input.global_admin_count + admins := get_array(input, "global_admins") + + output := { + "compliant": compliant, + "message": generate_message(admin_count), + "affected_resources": admins, + "details": { + "global_admin_count": admin_count, + "recommended_min": 2, + "recommended_max": 4 + } + } +} + +generate_message(admin_count) := msg if { + admin_count < 2 + msg := sprintf("Only %d global admin(s) found. Minimum 2 recommended for continuity.", [admin_count]) +} + +generate_message(admin_count) := msg if { + admin_count > 4 + msg := sprintf("%d global admins found. Maximum 4 recommended to minimize attack surface.", [admin_count]) +} + +generate_message(admin_count) := msg if { + admin_count >= 2 + admin_count <= 4 + msg := sprintf("%d global admins configured (within recommended range of 2-4)", [admin_count]) +} + +get_array(obj, key) := value if { + value := obj[key] +} else := [] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.2.1_no_unmanaged_public_groups.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.2.1_no_unmanaged_public_groups.rego new file mode 100644 index 00000000..cd2a041b --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.2.1_no_unmanaged_public_groups.rego @@ -0,0 +1,46 @@ +# METADATA +# title: Ensure that only organizationally managed/approved public groups exist +# description: Public groups should be reviewed and managed by the organization. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-1.2.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Group.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_1_2_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine public group configuration", + "details": {}, +} + +publics := object.get(input, "public_groups", []) if { true } + +compliant_value := true if { count(publics) == 0 } else := false if { true } + +msg := "No public groups exist" if { compliant_value } else := sprintf("%d public group(s) exist and require organizational approval", [count(publics)]) if { true } + +# This is a best-effort automated check. +# We treat any Public groups as requiring review and fail the control so it remains actionable. +result := output if { + output := { + "compliant": compliant_value, + "message": msg, + "details": { + "total_groups": input.total_groups, + "public_groups_count": count(publics), + "public_groups": [{"id": g.id, "displayName": g.displayName} | some g in publics], + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.1_password_expiration.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.1_password_expiration.rego new file mode 100644 index 00000000..0a3908b7 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.1_password_expiration.rego @@ -0,0 +1,63 @@ +# METADATA +# title: Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)' +# description: | +# Configure password policies to never expire. NIST and Microsoft recommend +# this approach when MFA is enabled, as forced password rotation leads to +# weaker passwords without providing security benefits. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-1.3.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Domain.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_1_3_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve domain password policy data", + "details": {} +} + +# 2147483647 = "never expires" (max int32) +never_expires := 2147483647 + +result := output if { + domains := get_array(input, "domains") + managed_domains := [d | some d in domains; d.is_managed == true] + non_compliant := [d | some d in managed_domains; d.password_validity_days != never_expires] + + output := { + "compliant": count(non_compliant) == 0, + "message": generate_message(managed_domains, non_compliant), + "affected_resources": [d.domain_name | some d in non_compliant], + "details": { + "total_managed_domains": count(managed_domains), + "compliant_domains": count(managed_domains) - count(non_compliant), + "non_compliant_domains": count(non_compliant), + "domains": [{"name": d.domain_name, "password_validity_days": d.password_validity_days} | some d in managed_domains] + } + } +} + +generate_message(managed_domains, non_compliant) := msg if { + count(non_compliant) == 0 + msg := sprintf("All %d managed domain(s) have password expiration disabled", [count(managed_domains)]) +} + +generate_message(managed_domains, non_compliant) := msg if { + count(non_compliant) > 0 + msg := sprintf("%d of %d managed domain(s) have password expiration enabled", [count(non_compliant), count(managed_domains)]) +} + +get_array(obj, key) := value if { + value := obj[key] +} else := [] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.4_user_owned_apps_restricted.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.4_user_owned_apps_restricted.rego new file mode 100644 index 00000000..0b0308a9 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.4_user_owned_apps_restricted.rego @@ -0,0 +1,51 @@ +# METADATA +# title: Ensure 'User owned apps and services' is restricted +# description: | +# Allowing users to own apps and services increases the risk of unauthorized +# application integrations and data exposure. Restrict this capability so that +# only approved administrators can enable and manage user apps and services. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-1.3.4 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_1_3_4 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve Microsoft 365 Apps and Services settings", + "details": {} +} + +result := output if { + enabled := input.user_owned_apps_enabled + + output := { + # CIS intent: restricted => disabled + "compliant": enabled == false, + "message": generate_message(enabled), + "affected_resources": generate_affected(enabled), + "details": { + "user_owned_apps_enabled": enabled, + "is_office_store_enabled": input.is_office_store_enabled + } + } +} + +generate_message(true) := "User owned apps and services are not restricted (enabled)" +generate_message(false) := "User owned apps and services are restricted (disabled)" +generate_message(null) := "Unable to determine whether user owned apps and services are restricted" + +generate_affected(false) := [] +generate_affected(true) := ["User owned apps and services are enabled"] +generate_affected(null) := ["User owned apps and services setting unknown"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.5_forms_internal_phishing_protection.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.5_forms_internal_phishing_protection.rego new file mode 100644 index 00000000..17342aa2 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.5_forms_internal_phishing_protection.rego @@ -0,0 +1,56 @@ +# METADATA +# title: Ensure internal phishing protection for Forms is enabled +# description: | +# Internal phishing protection helps prevent misuse of Microsoft Forms for +# phishing within the organization. Enable internal phishing protection to +# reduce the risk of credential harvesting and social engineering attacks. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-1.3.5 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Forms +# requires_permissions: +# - OrgSettings-Forms.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_1_3_5 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve Microsoft Forms settings", + "details": {} +} + +result := output if { + enabled := input.internal_phishing_protection_enabled + + output := { + "compliant": enabled == true, + "message": generate_message(enabled), + "affected_resources": generate_affected(enabled), + "details": { + "internal_phishing_protection_enabled": enabled, + "external_sharing_enabled": input.external_sharing_enabled, + "external_send_form_enabled": input.external_send_form_enabled, + "external_share_collaborating_enabled": input.external_share_collaborating_enabled, + "external_share_template_enabled": input.external_share_template_enabled, + "external_share_result_enabled": input.external_share_result_enabled, + "bing_search_enabled": input.bing_search_enabled, + "record_identity_by_default_enabled": input.record_identity_by_default_enabled + } + } +} + +generate_message(true) := "Microsoft Forms internal phishing protection is enabled" +generate_message(false) := "Microsoft Forms internal phishing protection is not enabled" +generate_message(null) := "Unable to determine Microsoft Forms internal phishing protection setting" + +generate_affected(true) := [] +generate_affected(false) := ["Microsoft Forms internal phishing protection is disabled"] +generate_affected(null) := ["Microsoft Forms internal phishing protection setting unknown"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.10_DMARC_records_published.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.10_DMARC_records_published.rego new file mode 100644 index 00000000..31cb8fc7 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.10_DMARC_records_published.rego @@ -0,0 +1,67 @@ +# METADATA +# title: Ensure DMARC Records for all Exchange Online domains are published +# description: | +# DMARC, or Domain-based Message Authentication, Reporting, and Conformance, +# assists recipient mail systems in determining the appropriate action to take when +# messages from a domain fail to meet SPF or DKIM authentication criteria. +# Ensure that the record exists that has the following flags defined either +# p=quarantine OR p=reject. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.10 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_10 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + domains := input.domains + + # Collect all domains where DMARC records are missing or empty + dmarc_issues := [d | d := domains[_]; not dmarc_record_published(d)] + + compliant := count(dmarc_issues) == 0 + + output := { + "compliant": compliant, + "message": generate_message(compliant, dmarc_issues), + "affected_resources": generate_affected_resources(compliant, dmarc_issues), + "details": { + "total_domains": count(domains), + "non_compliant_domains_count": count(dmarc_issues), + "non_compliant_domains": dmarc_issues + } + } +} + +dmarc_record_published(domain) if { + domain.dmarc_record + dmarc := lower(domain.dmarc_record) + dmarc != "" + startswith(dmarc, "v=dmarc1") + + tags := [trim_space(t) | t := split(dmarc, ";")[_]] + some i + tag := tags[i] + startswith(tag, "p=") + policy := trim_space(substring(tag, 2, count(tag)-2)) + policy in {"quarantine", "reject"} +} + +generate_message(true, _) := "All Exchange domains have DMARC records published." +generate_message(false, dmarc_issues) := sprintf( + "%d domain(s) do not meet DMARC enforcement requirements (missing record or p policy is not quarantine/reject)", + [count(dmarc_issues)] +) + +generate_affected_resources(true, _) := [] +generate_affected_resources(false, dmarc_issues) := [d.domain | d := dmarc_issues[_]] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.11_Comprehensive_Attachment_Filtering_Applied.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.11_Comprehensive_Attachment_Filtering_Applied.rego new file mode 100644 index 00000000..0986dc9c --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.11_Comprehensive_Attachment_Filtering_Applied.rego @@ -0,0 +1,110 @@ +# METADATA +# title: Ensure comprehensive attachment filtering is applied +# description: | +# The Common Attachment Types Filter lets a user block known and custom malicious file types +# from being attached to emails. The policy provided by Microsoft covers 53 extensions +# and an additional custom list of extensions can be defined. +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.11 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_11 + +import rego.v1 + +attach_exts := [ + "7z","a3x","ace","ade","adp","ani","app","appinstaller","applescript","application", + "appref-ms","appx","appxbundle","arj","asd","asx","bas","bat","bgi","bz2","cab", + "chm","cmd","com","cpl","crt","cs","csh","daa","dbf","dcr","deb","desktopthemepackfile", + "dex","diagcab","dif","dir","dll","dmg","doc","docm","dot","dotm","elf","eml","exe", + "fxp","gadget","gz","hlp","hta","htc","htm","html","hwpx","ics","img","inf","ins","iqy", + "iso","isp","jar","jnlp","js","jse","kext","ksh","lha","lib","library-ms","lnk","lzh", + "macho","mam","mda","mdb","mde","mdt","mdw","mdz","mht","mhtml","mof","msc","msi","msix", + "msp","msrcincident","mst","ocx","odt","ops","oxps","pcd","pif","plg","pot","potm","ppa", + "ppam","ppkg","pps","ppsm","ppt","pptm","prf","prg","ps1","ps11","ps11xml","ps1xml","ps2", + "ps2xml","psc1","psc2","pub","py","pyc","pyo","pyw","pyz","pyzw","rar","reg","rev","rtf", + "scf","scpt","scr","sct","searchConnector-ms","service","settingcontent-ms","sh","shb", + "shs","shtm","shtml","sldm","slk","so","spl","stm","svg","swf","sys","tar","theme","themepack", + "timer","uif","url","uue","vb","vbe","vbs","vhd","vhdx","vxd","wbk","website","wim","wiz", + "ws","wsc","wsf","wsh","xla","xlam","xlc","xll","xlm","xls","xlsb","xlsm","xlt","xltm","xlw", + "xnk","xps","xsl","xz","z" +] + +passing_value := 0.9 + +missing_exts[policy_identity] = missing if { + policy := input.malware_filter_policies[_] + policy_identity := policy.Identity + + missing := [ext | + ext := attach_exts[_] + not ext in policy.FileTypes + ] +} + +is_compliant[policy_identity] if { + policy := input.malware_filter_policies[_] + policy_identity := policy.Identity + + missing := missing_exts[policy_identity] + fail_threshold := count(attach_exts) * (1 - passing_value) + + count(missing) <= fail_threshold + policy.EnableFileFilter == true +} + +generate_message(true) := "Attachment filtering policy is correctly configured and enforced" +generate_message(false) := "Attachment filtering policy is misconfigured or not enforced" + +generate_affected_resources(true, _) := [] + +generate_affected_resources(false, data_input) := [ + pol.Identity | + pol := data_input.malware_filter_policies[_] +] + +policies := object.get(input, "malware_filter_policies", []) +has_policies := count(policies) > 0 + +non_compliant_policies := [ + pol.Identity | + pol := policies[_] + not is_compliant[pol.Identity] +] + +result := { + "compliant": false, + "message": "No malware filter policies found", + "affected_resources": ["MalwareFilterPolicy"], + "details": { + "malware_filter_policies_evaluated": 0, + "passing_threshold": passing_value, + "total_known_extensions": count(attach_exts) + } +} if { + not has_policies +} + +result := { + "compliant": count(non_compliant_policies) == 0, + "message": generate_message(count(non_compliant_policies) == 0), + "affected_resources": generate_affected_resources(count(non_compliant_policies) == 0, input), + "details": { + "malware_filter_policies_evaluated": count(policies), + "non_compliant_policies": non_compliant_policies, + "passing_threshold": passing_value, + "total_known_extensions": count(attach_exts) + } +} if { + has_policies +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.12_ConnectionFilter_IPAllowList_not_used.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.12_ConnectionFilter_IPAllowList_not_used.rego new file mode 100644 index 00000000..582482a6 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.12_ConnectionFilter_IPAllowList_not_used.rego @@ -0,0 +1,60 @@ +# METADATA +# title: Ensure the connection filter IP allow list is not used +# description: | +# In Microsoft 365 organizations with Exchange Online mailboxes or standalone +# Exchange Online Protection organizations without Exchange Online mailboxes, +# connection filtering and the default connection filter policy identify good or +# bad source email servers by IP addresses. The key components of the default connection +# filter policy are IP Allow List, IP Block List and Safe list. +# The recommended state is IP Allow List empty or undefined. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.12 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_12 + +import rego.v1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +policies := object.get(input, "connection_filter_policies", []) + +non_compliant_policies := [ + { + "identity": object.get(p, "Identity", object.get(p, "Name", null)), + "ip_allow_list": object.get(p, "IPAllowList", []) + } | + p := policies[_] + count(object.get(p, "IPAllowList", [])) > 0 +] + +compliant := count(non_compliant_policies) == 0 + +result := { + "compliant": compliant, + "message": messages[compliant], + "affected_resources": affected_resources[compliant], + "details": { + "policies_evaluated": count(policies), + "non_compliant_policies": non_compliant_policies + } +} + +messages := { + true: "IPAllowList is empty for all Exchange Online Hosted Connection Filter policies", + false: "IPAllowList is not empty for one or more Exchange Online Hosted Connection Filter policies" +} + +affected_resources := { + true: [], + false: ["HostedConnectionFilterPolicy"] +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.13_Connection_Filter_SafeList_Off.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.13_Connection_Filter_SafeList_Off.rego new file mode 100644 index 00000000..6718c79c --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.13_Connection_Filter_SafeList_Off.rego @@ -0,0 +1,45 @@ +# METADATA +# title: Ensure the connection filter safe list is off +# description: | +# In Microsoft 365 organizations with Exchange Online mailboxes or standalone +# Exchange Online Protection organizations without Exchange Online mailboxes +# connection filtering and the default connection filter policy identify good or bad +# source email servers by IP addresses. The key components of the default connection +# filter policy are IP Allow List, IP Block List and Safe list. +# The safe list is a pre-configured allow list that is dynamically updated by Microsoft. +# The recommended safe list state is: Off or False +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.13 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_13 + +import rego.v1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +enable_safe_list := object.get(input, "enable_safe_list", false) + +result := { + "compliant": true, + "message": "EnableSafeList is False for Exchange Online Hosted Connection Filter", + "affected_resources": [], + "details": {"EnableSafeList": enable_safe_list} +} if enable_safe_list == false + +result := { + "compliant": false, + "message": "EnableSafeList is not False for Exchange Online Hosted Connection Filter", + "affected_resources": ["HostedConnectionFilterPolicy"], + "details": {"EnableSafeList": enable_safe_list} +} if enable_safe_list == true diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.14_Inbound_AntiSpam_Policies_DoNot_AllowedDomains.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.14_Inbound_AntiSpam_Policies_DoNot_AllowedDomains.rego new file mode 100644 index 00000000..ecf75ec9 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.14_Inbound_AntiSpam_Policies_DoNot_AllowedDomains.rego @@ -0,0 +1,54 @@ +# METADATA +# title: Ensure inbound anti-spam policies do not contain allowed domains +# description: | +# Anti-spam protection is a feature of Exchange Online that utilizes policies +# to help to reduce the amount of junk email bulk and phishing emails a mailbox receives. +# These policies contain lists to allow or block specific senders or domains. +# • The allowed senders list +# • The allowed domains list +# • The blocked senders list +# • The blocked domains list +# The recommended state is: Do not define any Allowed domains +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.14 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_14 + +import rego.v1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +allowed_sender_domains := object.get(input, "allowed_sender_domains", []) + +allowed_sender_domains_undefined := true if count(allowed_sender_domains) == 0 +allowed_sender_domains_undefined := false if count(allowed_sender_domains) > 0 + +result := { + "compliant": allowed_sender_domains_undefined, + "message": messages[allowed_sender_domains_undefined], + "affected_resources": affected_resources[allowed_sender_domains_undefined], + "details": { + "allowed_sender_domains": allowed_sender_domains + } +} + +messages := { + true: "AllowedSenderDomains is undefined or empty for the policy", + false: "AllowedSenderDomains is defined for the policy" +} + +affected_resources := { + true: [], + false: ["HostedContentFilterPolicy"] +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.15_Outbound_AntiSpam_MessageLimits_InPlace.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.15_Outbound_AntiSpam_MessageLimits_InPlace.rego new file mode 100644 index 00000000..6915f106 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.15_Outbound_AntiSpam_MessageLimits_InPlace.rego @@ -0,0 +1,117 @@ +# METADATA +# title: Ensure outbound anti-spam message limits are in place +# description: | +# The default outbound anti-spam policy in Microsoft Defender automatically applies +# to all users and is designed to detect and limit suspicious email-sending behavior. +# The recommended state is: +# • External: Restrict sending to external recipients (per hour) - 500 +# • Internal: Restrict sending to internal recipients (per hour) - 1000 +# • Daily: Maximum recipient limit per day - 1000 +# • Action: Over limit action - Restrict the user from sending mail +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.15 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_15 + +import rego.v1 + +# Expected policy values +required_policy_fields := { + "RecipientLimitExternalPerHour": 500, + "RecipientLimitInternalPerHour": 1000, + "RecipientLimitPerDay": 1000, +} + +valid_actions := {"BlockUser", "RestrictUser", "Restrict"} +missing_sentinel := "__missing__" + +limits_compliant if input != null { + policy := input.default_policy + policy != null + + all_keys_correct := {k | k in required_policy_fields; policy[k] == required_policy_fields[k]} + count(all_keys_correct) == count(required_policy_fields) + + # Action must match one of the valid options + policy.ActionWhenThresholdReached in valid_actions +} + +limits_compliant := false if { + policy := input.default_policy + policy != null + some k + required_policy_fields[k] + object.get(policy, k, missing_sentinel) == missing_sentinel +} + +limits_compliant := false if { + policy := input.default_policy + policy != null + object.get(policy, "ActionWhenThresholdReached", missing_sentinel) == missing_sentinel +} + +limits_compliant := false if { + policy := input.default_policy + policy != null + some k + required_policy_fields[k] + object.get(policy, k, missing_sentinel) != missing_sentinel + policy[k] != required_policy_fields[k] +} + +limits_compliant := false if { + policy := input.default_policy + policy != null + object.get(policy, "ActionWhenThresholdReached", missing_sentinel) != missing_sentinel + not policy.ActionWhenThresholdReached in valid_actions +} + +policy := input.default_policy +has_policy := policy != null + +compliant := true if { + has_policy + limits_compliant +} + +compliant := false if { + has_policy + not limits_compliant +} + +compliant := false if { + not has_policy +} + +compliant_message := "Outbound spam filter policy is correctly configured for message limits and over-limit action" +non_compliant_message := "Outbound spam filter policy settings for message limits or over-limit action are misconfigured" +generate_message(true) := compliant_message +generate_message(false) := non_compliant_message + +generate_affected_resources(true, _) := [] +generate_affected_resources(false, _) := ["Outbound Spam Filter Policy"] + +result := { + "compliant": compliant, + "message": generate_message(compliant), + "affected_resources": generate_affected_resources(compliant, input), + "details": { + "RecipientLimitExternalPerHour": object.get(policy, "RecipientLimitExternalPerHour", null), + "RecipientLimitInternalPerHour": object.get(policy, "RecipientLimitInternalPerHour", null), + "RecipientLimitPerDay": object.get(policy, "RecipientLimitPerDay", null), + "ActionWhenThresholdReached": object.get(policy, "ActionWhenThresholdReached", null), + "required_policy_settings": required_policy_fields, + "valid_actions": valid_actions + } +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.1_SafeLinks_OfficeApplications_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.1_SafeLinks_OfficeApplications_Enabled.rego new file mode 100644 index 00000000..930f55d0 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.1_SafeLinks_OfficeApplications_Enabled.rego @@ -0,0 +1,123 @@ +# METADATA +# title: Ensure Safe Links for Office Applications is Enabled +# description: | +# Enabling Safe Links policy for Office applications allows URL's that exist +# inside of Office documents and email applications opened by Office, Office Online +# and Office mobile to be processed against Defender. +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_1 + +import rego.v1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +required_fields := { + "EnableSafeLinksForEmail": true, + "EnableSafeLinksForTeams": true, + "EnableSafeLinksForOffice": true, + "TrackClicks": true, + "AllowClickThrough": false, + "ScanUrls": true, + "EnableForInternalSenders": true, + "DeliverMessageAfterScan": true, + "DisableUrlRewrite": false +} + +missing_sentinel := "__missing__" + +field_non_compliant(p, f) if { + object.get(p, f, missing_sentinel) == missing_sentinel +} + +field_non_compliant(p, f) if { + object.get(p, f, missing_sentinel) != missing_sentinel + p[f] != required_fields[f] +} + +non_compliant_fields(p) := fields if { + fields := {f | + some f + required_fields[f] = _ # f is a key in required_fields (even if value is false) + field_non_compliant(p, f) + } +} + +policy_compliant(p) := true if { + count(non_compliant_fields(p)) == 0 +} + +policy_compliant(p) := false if { + count(non_compliant_fields(p)) > 0 +} + +generate_message(true, _) := "All Safe Links policies for Office applications are compliant" +generate_message_no_policies := "No Safe Links policies found for Office applications" + +generate_message(false, non_compliant) := sprintf( + "%d Safe Links policy(ies) for Office applications are not compliant", + [count(non_compliant)] +) + +generate_affected_resources(true, _) := [] + +generate_affected_resources(false, non_compliant) := resources if { + resources := [ + { + "identity": object.get(p, "Identity", object.get(p, "Name", null)), + "non_compliant_fields": [f | f := non_compliant_fields(p)[_]] + } | + p := non_compliant[_] + ] +} + +no_policies_result := { + "compliant": false, + "message": generate_message_no_policies, + "affected_resources": [{"identity": "Safe Links policy", "non_compliant_fields": ["missing_policy"]}], + "details": { + "policies_checked": [], + "required_fields": required_fields + } +} + +with_policies_result(policies, non_compliant) := { + "compliant": count(non_compliant) == 0, + "message": generate_message(count(non_compliant) == 0, non_compliant), + "affected_resources": generate_affected_resources(count(non_compliant) == 0, non_compliant), + "details": { + "policies_checked": [object.get(p, "Identity", object.get(p, "Name", null)) | p := policies[_]], + "required_fields": required_fields + } +} + +result := output if { + policies := object.get(input, "safe_links_policies", []) + count(policies) == 0 + output := no_policies_result +} + +result := output if { + policies := object.get(input, "safe_links_policies", []) + count(policies) > 0 + + non_compliant := [ + p | + p := policies[_] + not policy_compliant(p) + ] + + output := with_policies_result(policies, non_compliant) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.2_Common_AttachmentTypes_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.2_Common_AttachmentTypes_Enabled.rego new file mode 100644 index 00000000..a21a1916 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.2_Common_AttachmentTypes_Enabled.rego @@ -0,0 +1,53 @@ +# METADATA +# title: Ensure the Common Attachment Types Filter is enabled +# description: | +# Common Attachment Types Filter lets a user block known and +# custom malicious file types from being attached to emails +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_2 + +import rego.v1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +file_filter_enabled := true if input.enable_file_filter == true +file_filter_enabled := false if input.enable_file_filter != true + +result := output if { + compliant := file_filter_enabled == true + + output := { + "compliant": compliant, + "message": generate_message(file_filter_enabled), + "affected_resources": generate_affected_resources(file_filter_enabled), + "details": { + "enablefilefilter": file_filter_enabled + } + } +} + +generate_message(file_filter_enabled) := msg if { + file_filter_enabled == true + msg := "The 'Enable the common attachments filter' is On." +} + +generate_message(file_filter_enabled) := msg if { + file_filter_enabled == false + msg := "The 'Enable the common attachments filter' is Off." +} + +generate_affected_resources(true) := [] +generate_affected_resources(false) := ["Common attachments filter is disabled"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.3_Notifications_InternalUsers_sendingMalware_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.3_Notifications_InternalUsers_sendingMalware_Enabled.rego new file mode 100644 index 00000000..9c67cc3a --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.3_Notifications_InternalUsers_sendingMalware_Enabled.rego @@ -0,0 +1,84 @@ +# METADATA +# title: Ensure Notifications for Internal Users Sending Malware is Enabled +# description: | +# This control ensures that administrators are notified when internal users send malware. +# It helps security teams respond promptly to malware incidents. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_3 + +import rego.v1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +required_values := { + "EnableInternalSenderAdminNotifications": true, + "InternalSenderAdminAddress": "" # empty value considered compliant +} + +missing_sentinel := "__missing__" + +field_non_compliant(p, f) if { + object.get(p, f, missing_sentinel) == missing_sentinel +} + +field_non_compliant(p, f) if { + object.get(p, f, missing_sentinel) != missing_sentinel + p[f] != required_values[f] +} + +policy_compliant(p) if { + invalid_fields := {f | + some f + required_values[f] + field_non_compliant(p, f) + } + count(invalid_fields) == 0 +} + +policy := input.default_policy +policies := [policy] if policy != null +policies := [] if policy == null + +non_compliant_policies := [ + {"policy": p.Name, "failed_fields": [f | + some f + required_values[f] + field_non_compliant(p, f) + ]} | + p := policies[_] + not policy_compliant(p) +] + +result := { + "compliant": true, + "message": sprintf("All %d notification policies are compliant", [count(policies)]) +} if { + count(policies) > 0 + count(non_compliant_policies) == 0 +} + +result := { + "compliant": false, + "message": sprintf("Non-compliant policies detected: %v", [non_compliant_policies]) +} if { + count(non_compliant_policies) > 0 +} + +result := { + "compliant": false, + "message": "No notification policies found" +} if { + count(policies) == 0 +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.4_Safe_AttachementsPolicy_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.4_Safe_AttachementsPolicy_Enabled.rego new file mode 100644 index 00000000..48d5fc1c --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.4_Safe_AttachementsPolicy_Enabled.rego @@ -0,0 +1,122 @@ +# METADATA +# title: Ensure Safe Attachments policy is enabled +# description: | +# The Safe Attachments policy helps protect users from malware in email attachments +# by scanning attachments for viruses, malware, and other malicious content. +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.4 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_4 + +import rego.v1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +target_policy_candidate(p) if { + p.Identity == "Built-In Protection Policy" +} + +target_policy_candidate(p) if { + p.Name == "Built-In Protection Policy" +} + +target_policies := [p | + p := input.safe_attachment_policies[_] + target_policy_candidate(p) +] + +target_policy := target_policies[0] if count(target_policies) > 0 + +policy_identity := target_policy.Identity if target_policy.Identity +policy_identity := target_policy.Name if target_policy.Name + +has_policy := target_policy != null + +policy_matches if { + target_policy.Enable == true + target_policy.Action == "Block" + target_policy.QuarantineTag == "AdminOnlyAccessPolicy" + policy_identity == "Built-In Protection Policy" +} + +compliant := true if { + has_policy + policy_matches +} + +compliant := false if { + has_policy + not policy_matches +} + +compliant := null if { + not has_policy +} + +result := { + "compliant": compliant, + "message": message, + "affected_resources": affected_resources, + "details": { + "identity": policy_identity, + "enable": target_policy.Enable, + "action": target_policy.Action, + "quarantine_tag": target_policy.QuarantineTag + } +} if { + has_policy +} + +message := "Safe Attachments Built-In Protection Policy is enabled, blocking threats, and correctly configured." if compliant == true +message := "Unable to determine Safe Attachments policy configuration." if compliant == null +message := "Safe Attachments Built-In Protection Policy is disabled." if { + compliant == false + target_policy.Enable == false +} +message := "Safe Attachments policy action is not set to 'Block'." if { + compliant == false + target_policy.Enable == true + target_policy.Action != "Block" +} +message := "Safe Attachments policy does not use the 'AdminOnlyAccessPolicy' quarantine tag." if { + compliant == false + target_policy.Enable == true + target_policy.Action == "Block" + target_policy.QuarantineTag != "AdminOnlyAccessPolicy" +} + +affected_resources := [] if compliant == true +affected_resources := ["Safe Attachments policy status unknown"] if compliant == null +affected_resources := [sprintf("Non-compliant Safe Attachments policy: %v", [policy_identity])] if { + compliant == false + policy_identity +} +affected_resources := ["Non-compliant Safe Attachments policy: Built-In Protection Policy"] if { + compliant == false + not policy_identity +} + +result := { + "compliant": false, + "message": "Unable to determine Safe Attachments policy configuration.", + "affected_resources": ["Safe Attachments policy status unknown"], + "details": { + "identity": null, + "enable": null, + "action": null, + "quarantine_tag": null + } +} if { + not has_policy +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.5_Safe_Attachments_SharePoint_OneDrive_MSTeams_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.5_Safe_Attachments_SharePoint_OneDrive_MSTeams_Enabled.rego new file mode 100644 index 00000000..ff8aae1d --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.5_Safe_Attachments_SharePoint_OneDrive_MSTeams_Enabled.rego @@ -0,0 +1,84 @@ +# METADATA +# title: Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled +# description: | +# Safe Attachments for SharePoint, OneDrive, and Microsoft Teams scans these services for malicious files. +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.5 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_5 + +import rego.v1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +required_values := { + "EnableATPForSPOTeamsODB": true, + "EnableSafeDocs": true, + "AllowSafeDocsOpen": false +} + +missing_sentinel := "__missing__" + +field_non_compliant(p, f) if { + object.get(p, f, missing_sentinel) == missing_sentinel +} + +field_non_compliant(p, f) if { + object.get(p, f, missing_sentinel) != missing_sentinel + p[f] != required_values[f] +} + +policy_compliant(p) if { + invalid_fields := {f | + some f + required_values[f] = _ + field_non_compliant(p, f) + } + count(invalid_fields) == 0 +} + +policy := input.atp_policy +policies := [policy] if policy != null +policies := [] if policy == null + +non_compliant_policies := [ {"policy": p.Name, "failed_fields": [f | + some f + required_values[f] = _ + field_non_compliant(p, f) +]} | + p := policies[_] + not policy_compliant(p) +] + +result := { + "compliant": true, + "message": sprintf("All %d Safe Attachments policies are compliant", [count(policies)]) +} if { + count(policies) > 0 + count(non_compliant_policies) == 0 +} + +result := { + "compliant": false, + "message": sprintf("Non-compliant policies detected: %v", [non_compliant_policies]) +} if { + count(non_compliant_policies) > 0 +} + +result := { + "compliant": false, + "message": "No Safe Attachments policies found" +} if { + count(policies) == 0 +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.6_Exchange_OnlineSpam_Policies_Notify_Administrators.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.6_Exchange_OnlineSpam_Policies_Notify_Administrators.rego new file mode 100644 index 00000000..250af297 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.6_Exchange_OnlineSpam_Policies_Notify_Administrators.rego @@ -0,0 +1,79 @@ +# METADATA +# title: Ensure Exchange Online Spam Policies are set to notify administrators +# description: | +# In Microsoft 365 organizations with mailboxes in Exchange Online or +# standalone Exchange Online Protection organizations without Exchange +# Online mailboxes, email messages are automatically protected against spam by EOP. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations benchmark +# custom: +# control_id: CIS-2.1.6 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_6 + +import rego.v1 + +bcc_suspicious_outbound_mail := object.get(input, "bcc_suspicious_outbound_mail", false) +notify_outbound_spam := object.get(input, "notify_outbound_spam", false) +auto_forwarding_mode := object.get(input, "auto_forwarding_mode", null) + +missing_bcc if { + not bcc_suspicious_outbound_mail +} + +missing_notify if { + not notify_outbound_spam +} + +missing_settings := array.concat( + [s | s := "bcc_suspicious_outbound_mail"; missing_bcc], + [s | s := "notify_outbound_spam"; missing_notify] +) + +outbound_spam_monitoring_enabled if { + count(missing_settings) == 0 +} + +scan_result = { + "compliant": true, + "message": "Outbound spam BCC and notification settings are enabled", + "affected_resources": [], + "details": { + "bcc_suspicious_outbound_mail": bcc_suspicious_outbound_mail, + "notify_outbound_spam": notify_outbound_spam, + "auto_forwarding_mode": auto_forwarding_mode + } +} if { + outbound_spam_monitoring_enabled +} + +scan_result = { + "compliant": false, + "message": generate_message_missing, + "affected_resources": ["HostedOutboundSpamFilterPolicy"], + "details": { + "bcc_suspicious_outbound_mail": bcc_suspicious_outbound_mail, + "notify_outbound_spam": notify_outbound_spam, + "auto_forwarding_mode": auto_forwarding_mode + } +} if { + not outbound_spam_monitoring_enabled +} + +result := scan_result + +generate_message_missing := msg if { + count(missing_settings) > 0 + msg := sprintf( + "Outbound spam settings disabled or misconfigured: %v", + missing_settings + ) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.7_AntiPhishing_Policy_is_created.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.7_AntiPhishing_Policy_is_created.rego new file mode 100644 index 00000000..69d4b4b1 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.7_AntiPhishing_Policy_is_created.rego @@ -0,0 +1,118 @@ +# METADATA +# title: Ensure that an anti-phishing policy has been created +# description: | +# By default, Office 365 includes built-in features that help protect users from phishing attacks. +# Set up anti-phishing policies to increase this protection, for example by refining settings +# to better detect and prevent impersonation and spoofing attacks. The default policy applies +# to all users within the organization and is a single view to fine-tune anti-phishing protection. +# Custom policies can be created and configured for specific users, groups or domains within the organization +# and will take precedence over the default policy for the scoped users. +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.7 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_7 + +import rego.v1 + +required_values := { + "Enabled": true, + "PhishThresholdLevel": 3, + "EnableTargetedUserProtection": true, + "EnableOrganizationDomainsProtection": true, + "EnableMailboxIntelligence": true, + "EnableMailboxIntelligenceProtection": true, + "EnableSpoofIntelligence": true, + "TargetedUserProtectionAction": "Quarantine", + "TargetedDomainProtectionAction": "Quarantine", + "MailboxIntelligenceProtectionAction": "Quarantine", + "EnableFirstContactSafetyTips": true, + "EnableSimilarUsersSafetyTips": true, + "EnableSimilarDomainsSafetyTips": true, + "EnableUnusualCharactersSafetyTips": true, + "HonorDmarcPolicy": true +} + +missing_sentinel := "__missing__" + +field_non_compliant(p, f) if { + object.get(p, f, missing_sentinel) == missing_sentinel +} + +field_non_compliant(p, f) if { + object.get(p, f, missing_sentinel) != missing_sentinel + p[f] != required_values[f] +} + +policy_compliant(p) if { + invalid_fields := {f | + some f + required_values[f] + field_non_compliant(p, f) + } + count(invalid_fields) == 0 +} + +policies := object.get(input, "anti_phish_policies", []) + +non_compliant_policies := [ + {"policy": p.Name, "failed_fields": [f | + some f + required_values[f] + field_non_compliant(p, f) + ]} | + p := policies[_] + not policy_compliant(p) +] + +targeted_users := [user | + p := policies[_] + policy_compliant(p) + users := object.get(p, "TargetedUsersToProtect", []) + user := users[_] +] + +global_compliant_policies := [p | + p := policies[_] + policy_compliant(p) + count(object.get(p, "TargetedUsersToProtect", [])) == 0 +] + +result := { + "compliant": false, + "message": "No anti-phishing policies found" +} if { + count(policies) == 0 +} + +result := { + "compliant": false, + "message": sprintf( + "Non-compliant policies detected: %v", + [non_compliant_policies] + ) +} if { + count(policies) > 0 + count(non_compliant_policies) > 0 +} + +result := { + "compliant": true, + "message": sprintf( + "Found %d user(s) protected by compliant anti-phishing policy (including global/default)", + [count(targeted_users) + count(global_compliant_policies)] + ) +} if { + count(policies) > 0 + count(non_compliant_policies) == 0 +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.8_SPF_records_published.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.8_SPF_records_published.rego new file mode 100644 index 00000000..14118661 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.8_SPF_records_published.rego @@ -0,0 +1,58 @@ +# METADATA +# title: Ensure that SPF records are published for all Exchange Domains +# description: | +# A corresponding Sender Policy Framework (SPF) record +# should be created for each domain that will be configured in Exchange. +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.8 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_8 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + domains := input.domains + + # Collect all domains where SPF records are missing or empty + spf_issues := [d | d := domains[_]; not spf_record_published(d)] + + compliant := count(spf_issues) == 0 + + output := { + "compliant": compliant, + "message": generate_message(compliant, spf_issues), + "affected_resources": generate_affected_resources(compliant, spf_issues), + "details": { + "total_domains": count(domains), + "non_compliant_domains_count": count(spf_issues), + "non_compliant_domains": spf_issues + } + } +} + +spf_record_published(domain) if { + record := trim(domain.spf_record, " \t\r\n") + record != "" + startswith(lower(record), "v=spf1") +} + +generate_message(true, _) := "All Exchange domains have SPF records published." + +generate_message(false, spf_issues) := sprintf( + "%d domain(s) do not have a valid SPF record (v=spf1...) published", + [count(spf_issues)] +) + +generate_affected_resources(true, _) := [] +generate_affected_resources(false, spf_issues) := [d.domain | d := spf_issues[_]] \ No newline at end of file diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.9_DKIM_is_enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.9_DKIM_is_enabled.rego new file mode 100644 index 00000000..a4da6a8c --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.9_DKIM_is_enabled.rego @@ -0,0 +1,66 @@ +# METADATA +# title: Ensure that DKIM is enabled for all Exchange Online Domains +# description: | +# DKIM is one of the trio of Authentication methods (SPF, DKIM and DMARC) that help +# prevent attackers from sending messages that look like they come from your domain. +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.1.9 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_1_9 + +import rego.v1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +# Compute dkim_enabled from per-domain lists +dkim_enabled := true if count(input.domains_with_dkim_disabled) == 0 +dkim_enabled := false if count(input.domains_with_dkim_disabled) > 0 +dkim_enabled := null if { + not input.domains_with_dkim_enabled + not input.domains_with_dkim_disabled +} + +result := output if { + compliant := dkim_enabled == true + + output := { + "compliant": compliant, + "message": generate_message(dkim_enabled), + "affected_resources": generate_affected_resources(dkim_enabled, input), + "details": { + "dkim_signing_enabled": dkim_enabled, + "domains_with_dkim_enabled": input.domains_with_dkim_enabled, + "domains_with_dkim_disabled": input.domains_with_dkim_disabled + } + } +} + +generate_message(dkim_enabled) := msg if { + dkim_enabled == true + msg := "DKIM signing is enabled for Exchange Online domains" +} + +generate_message(dkim_enabled) := msg if { + dkim_enabled == false + msg := "DKIM signing is disabled for Exchange Online domains" +} + +generate_message(dkim_enabled) := msg if { + dkim_enabled == null + msg := "Unable to determine DKIM signing status" +} + +generate_affected_resources(true, _) := [] +generate_affected_resources(false, data_input) := data_input.domains_with_dkim_disabled +generate_affected_resources(null, _) := ["DKIM signing status unknown"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.4.4_Zero_hour_AutoPurge_MSTeams_is_On.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.4.4_Zero_hour_AutoPurge_MSTeams_is_On.rego new file mode 100644 index 00000000..b83d0f57 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.4.4_Zero_hour_AutoPurge_MSTeams_is_On.rego @@ -0,0 +1,50 @@ +# METADATA +# title: Ensure Zero-hour auto purge for Microsoft Teams is on +# description: | +# Zero-hour auto purge is a protection feature that retroactively detects +# and neutralizes malware and high-confidence phishing. When ZAP for Teams protection +# blocks a message, the message is blocked for everyone in the chat. +# The initial block happens right after delivery, but ZAP occurs up to 48 hours after delivery. +# +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-2.4.4 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_2_4_4 + +import rego.v1 + +zap_enabled := object.get(input, "zap_enabled", null) + +zero_hour_auto_purge_enabled := true if zap_enabled == true +zero_hour_auto_purge_enabled := false if zap_enabled == false +zero_hour_auto_purge_enabled := null if zap_enabled == null + +result := output if { + compliant := zero_hour_auto_purge_enabled == true + + output := { + "compliant": compliant, + "message": generate_message(zero_hour_auto_purge_enabled), + "affected_resources": generate_affected_resources(zero_hour_auto_purge_enabled), + "details": { + "zap_enabled": zap_enabled + } + } +} + +generate_message(true) := "Zero-hour auto purge is enabled for Microsoft Teams" +generate_message(false) := "Zero-hour auto purge is not enabled for Microsoft Teams" +generate_message(null) := "Unable to determine Zero-hour auto purge status for Microsoft Teams" +generate_affected_resources(true) := [] +generate_affected_resources(false) := ["TeamsProtectionPolicy"] +generate_affected_resources(null) := ["TeamsProtectionPolicy status unknown"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/4.1_mark_unmanaged_devices_not_compliant.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/4.1_mark_unmanaged_devices_not_compliant.rego new file mode 100644 index 00000000..0355b0f7 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/4.1_mark_unmanaged_devices_not_compliant.rego @@ -0,0 +1,55 @@ +# METADATA +# title: Ensure devices without a compliance policy are marked 'not compliant' +# description: Mark devices without compliance policy as non-compliant. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/mem/intune/protect/actions-for-noncompliance +# description: Intune actions for noncompliance (conceptual) +# custom: +# control_id: CIS-4.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Intune +# requires_permissions: +# - DeviceManagementConfiguration.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_4_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine Intune compliance defaults", + "details": {}, +} + +# Compliant when both secureByDefault and scheduled noncompliance actions are enabled. +compliant if { + input.secure_by_default == true + input.is_scheduled_action_enabled == true +} + +compliant_value := true if { compliant } else := false if { true } + +msg := "Devices without a compliance policy are treated as not compliant (secureByDefault & scheduled actions enabled)" if { compliant } else := "Intune compliance defaults are not sufficiently strict" if { true } + +# Heuristic based on tenant settings: +# - secureByDefault should be true +# - isScheduledActionEnabled should be true (scheduled actions for noncompliance) +result := output if { + secure_by_default := input.secure_by_default + scheduled := input.is_scheduled_action_enabled + + output := { + "compliant": compliant_value, + "message": msg, + "details": { + "secure_by_default": secure_by_default, + "is_scheduled_action_enabled": scheduled, + "device_compliance_checkin_threshold_days": input.device_compliance_on_boarded, + "compliance_policy_summaries_count": count(input.compliance_policy_summaries), + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/4.2_block_personal_device_enrollment.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/4.2_block_personal_device_enrollment.rego new file mode 100644 index 00000000..90a87100 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/4.2_block_personal_device_enrollment.rego @@ -0,0 +1,44 @@ +# METADATA +# title: Ensure device enrollment for personally owned devices is blocked by default +# description: Block personal device enrollment by default. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-4.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Intune +# requires_permissions: +# - DeviceManagementServiceConfig.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_4_2 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine personal device enrollment restriction", + "details": {}, +} + +compliant_value := true if { input.personal_devices_blocked == true } else := false if { true } + +msg := "Personal device enrollment is blocked by default" if { input.personal_devices_blocked == true } else := "Personal device enrollment is not blocked by default" if { input.personal_devices_blocked == false } else := "Unable to determine personal device enrollment restriction" if { true } + +result := output if { + blocked := input.personal_devices_blocked + + output := { + "compliant": compliant_value, + "message": msg, + "details": { + "personal_devices_blocked": blocked, + "total_configurations": input.total_configurations, + "platform_restrictions_count": count(input.platform_restrictions), + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.2.2_block_third_party_integrated_apps.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.2.2_block_third_party_integrated_apps.rego new file mode 100644 index 00000000..b979d936 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.2.2_block_third_party_integrated_apps.rego @@ -0,0 +1,42 @@ +# METADATA +# title: Ensure third party integrated applications are not allowed +# description: Block third-party application registration. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-5.1.2.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_2_2 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine allowedToCreateApps", + "details": {}, +} + +compliant_value := true if { input.allowed_to_create_apps == false } else := false if { true } + +msg := "Third party integrated applications are not allowed (allowedToCreateApps=false)" if { input.allowed_to_create_apps == false } else := "Third party integrated applications are allowed (allowedToCreateApps=true)" if { input.allowed_to_create_apps == true } else := "Unable to determine allowedToCreateApps" if { true } + +result := out if { + value := input.allowed_to_create_apps + + out := { + "compliant": compliant_value, + "message": msg, + "details": { + "allowed_to_create_apps": value, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.2.3_restrict_tenant_creation.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.2.3_restrict_tenant_creation.rego new file mode 100644 index 00000000..cdb8a239 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.2.3_restrict_tenant_creation.rego @@ -0,0 +1,42 @@ +# METADATA +# title: Ensure 'Restrict non-admin users from creating tenants' is set to 'Yes' +# description: Prevent non-admin users from creating tenants. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-5.1.2.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_2_3 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine allowedToCreateTenants", + "details": {}, +} + +compliant_value := true if { input.allowed_to_create_tenants == false } else := false if { true } + +msg := "Non-admin users cannot create tenants (allowedToCreateTenants=false)" if { input.allowed_to_create_tenants == false } else := "Non-admin users can create tenants (allowedToCreateTenants=true)" if { input.allowed_to_create_tenants == true } else := "Unable to determine allowedToCreateTenants" if { true } + +result := out if { + value := input.allowed_to_create_tenants + + out := { + "compliant": compliant_value, + "message": msg, + "details": { + "allowed_to_create_tenants": value, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.3.1_dynamic_guest_group_exists.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.3.1_dynamic_guest_group_exists.rego new file mode 100644 index 00000000..61e7f992 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.3.1_dynamic_guest_group_exists.rego @@ -0,0 +1,73 @@ +# METADATA +# title: Ensure a dynamic group for guest users is created +# description: Create a dynamic group to manage guest users. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/identity/users/groups-dynamic-membership +# description: Dynamic membership rules (conceptual) +# custom: +# control_id: CIS-5.1.3.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Group.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_3_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "No dynamic guest user group detected", + "details": {}, +} + +# Look for dynamic membership rules referencing Guest user type. +is_guest_rule(rule) if { + lower(rule) != "" + contains(lower(rule), "guest") +} + +compliant_value := true if { + dyn := input.dynamic_groups + matching := [g | + some g in dyn + rule := g.membershipRule + rule != null + is_guest_rule(rule) + ] + count(matching) > 0 +} else := false if { true } + +msg := sprintf("Found %d dynamic group(s) targeting guest users", [count(matching)]) if { + dyn := input.dynamic_groups + matching := [g | + some g in dyn + rule := g.membershipRule + rule != null + is_guest_rule(rule) + ] + count(matching) > 0 +} else := "No dynamic guest user group detected" if { true } + +result := output if { + dyn := input.dynamic_groups + matching := [g | + some g in dyn + rule := g.membershipRule + rule != null + is_guest_rule(rule) + ] + + output := { + "compliant": compliant_value, + "message": msg, + "details": { + "dynamic_groups_count": input.dynamic_groups_count, + "matching_groups": [{"id": g.id, "displayName": g.displayName, "membershipRule": g.membershipRule} | some g in matching], + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.3.2_block_security_group_creation.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.3.2_block_security_group_creation.rego new file mode 100644 index 00000000..9582d044 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.3.2_block_security_group_creation.rego @@ -0,0 +1,42 @@ +# METADATA +# title: Ensure users cannot create security groups +# description: Prevent users from creating security groups. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-5.1.3.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_3_2 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine allowedToCreateSecurityGroups", + "details": {}, +} + +compliant_value := true if { input.allowed_to_create_security_groups == false } else := false if { true } + +msg := "Users cannot create security groups (allowedToCreateSecurityGroups=false)" if { input.allowed_to_create_security_groups == false } else := "Users can create security groups (allowedToCreateSecurityGroups=true)" if { input.allowed_to_create_security_groups == true } else := "Unable to determine allowedToCreateSecurityGroups" if { true } + +result := out if { + value := input.allowed_to_create_security_groups + + out := { + "compliant": compliant_value, + "message": msg, + "details": { + "allowed_to_create_security_groups": value, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.1_restrict_device_join.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.1_restrict_device_join.rego new file mode 100644 index 00000000..3e797df1 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.1_restrict_device_join.rego @@ -0,0 +1,52 @@ +# METADATA +# title: Ensure the ability to join devices to Entra is restricted +# description: Restrict device join to authorized users. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/identity/devices/device-join +# description: Device join settings (conceptual) +# custom: +# control_id: CIS-5.1.4.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.DeviceConfiguration + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_4_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine device join restrictions", + "details": {}, +} + +compliant if { + input.azure_ad_join_allowed_users != null + input.azure_ad_join_allowed_users != "all" +} + +compliant_value := true if { compliant } else := false if { true } + +msg := "Device join is restricted to selected users/groups" if { compliant } else := "Device join appears to be allowed broadly (allowedUsers=all or missing)" if { true } + +result := output if { + users := input.azure_ad_join_allowed_users + groups := input.azure_ad_join_allowed_groups + + # Compliant when join is not broadly allowed to everyone. + # We treat "all" / empty / null as non-compliant. + output := { + "compliant": compliant_value, + "message": msg, + "details": { + "allowed_users": users, + "allowed_groups": groups, + "user_device_quota": input.user_device_quota, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.6_restrict_bitlocker_key_recovery.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.6_restrict_bitlocker_key_recovery.rego new file mode 100644 index 00000000..9417d80c --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.6_restrict_bitlocker_key_recovery.rego @@ -0,0 +1,42 @@ +# METADATA +# title: Ensure users are restricted from recovering BitLocker keys +# description: Restrict BitLocker key recovery to admins. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-5.1.4.6 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_4_6 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine allowedToReadBitlockerKeysForOwnedDevice", + "details": {}, +} + +compliant_value := true if { input.allowed_to_read_bitlocker_keys_for_owned_device == false } else := false if { true } + +msg := "Users are restricted from recovering BitLocker keys (allowedToReadBitlockerKeysForOwnedDevice=false)" if { input.allowed_to_read_bitlocker_keys_for_owned_device == false } else := "Users can recover BitLocker keys (allowedToReadBitlockerKeysForOwnedDevice=true)" if { input.allowed_to_read_bitlocker_keys_for_owned_device == true } else := "Unable to determine allowedToReadBitlockerKeysForOwnedDevice" if { true } + +result := out if { + value := input.allowed_to_read_bitlocker_keys_for_owned_device + + out := { + "compliant": compliant_value, + "message": msg, + "details": { + "allowed_to_read_bitlocker_keys_for_owned_device": value, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.5.1_block_user_app_consent.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.5.1_block_user_app_consent.rego new file mode 100644 index 00000000..cc9cd59b --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.5.1_block_user_app_consent.rego @@ -0,0 +1,71 @@ +# METADATA +# title: Ensure user consent to apps accessing company data on their behalf is not allowed +# description: Block user consent to applications for company data. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent +# description: Configure user consent settings in Entra ID +# custom: +# control_id: CIS-5.1.5.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_5_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine user consent settings (permissionGrantPoliciesAssigned)", + "details": {}, +} + +compliant if { + perms := input.default_user_role_permissions + assigned := perms.permissionGrantPoliciesAssigned + assigned == [] +} + +compliant_value := true if { compliant } else := false if { true } + +assigned_count := count(assigned) if { + perms := input.default_user_role_permissions + assigned := perms.permissionGrantPoliciesAssigned + assigned != null +} else := null if { true } + +msg := "User consent to apps is disabled (permissionGrantPoliciesAssigned is empty)" if { + compliant +} else := sprintf( + "User consent to apps is enabled/restricted (permissionGrantPoliciesAssigned has %d entries)", + [assigned_count], +) if { + perms := input.default_user_role_permissions + assigned := perms.permissionGrantPoliciesAssigned + assigned != null + not compliant +} else := "Unable to determine user consent settings (permissionGrantPoliciesAssigned missing)" if { true } + +# According to Graph, user consent is controlled by authorizationPolicy.defaultUserRolePermissions.permissionGrantPoliciesAssigned. +# CIS intent: user consent should be disabled (empty list). +result := out if { + perms := input.default_user_role_permissions + assigned := perms.permissionGrantPoliciesAssigned + + is_empty := assigned == [] # strict: disabled + ok := is_empty + + out := { + "compliant": compliant_value, + "message": msg, + "details": { + "permission_grant_policies_assigned": assigned, + "permission_grant_policies_assigned_count": assigned_count, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.5.2_admin_consent_workflow_enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.5.2_admin_consent_workflow_enabled.rego new file mode 100644 index 00000000..54f7c38f --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.5.2_admin_consent_workflow_enabled.rego @@ -0,0 +1,50 @@ +# METADATA +# title: Ensure the admin consent workflow is enabled +# description: Enable admin consent workflow for apps. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow +# description: Configure the admin consent workflow +# custom: +# control_id: CIS-5.1.5.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_5_2 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve admin consent request policy", + "details": {}, +} + +compliant_value := true if { input.is_enabled == true } else := false if { true } + +msg := "Admin consent workflow is enabled" if { input.is_enabled == true } else := "Admin consent workflow is disabled" if { input.is_enabled == false } else := "Unable to determine admin consent workflow state" if { true } + +result := output if { + enabled := input.is_enabled + reviewers := input.reviewers + + has_reviewers := count(reviewers) > 0 + + output := { + "compliant": compliant_value, + "message": msg, + "details": { + "is_enabled": enabled, + "reviewers_count": count(reviewers), + "has_reviewers": has_reviewers, + "notify_reviewers": input.notify_reviewers, + "reminders_enabled": input.reminders_enabled, + "request_duration_in_days": input.request_duration_in_days, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.6.1_restrict_collaboration_invite_domains.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.6.1_restrict_collaboration_invite_domains.rego new file mode 100644 index 00000000..1c447635 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.6.1_restrict_collaboration_invite_domains.rego @@ -0,0 +1,57 @@ +# METADATA +# title: Ensure that collaboration invitations are sent to allowed domains only +# description: Restrict external collaboration to allowed domains/tenants. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/graph/api/crosstenantaccesspolicy-overview +# description: Cross-tenant access settings +# custom: +# control_id: CIS-5.1.6.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_6_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine cross-tenant access restrictions", + "details": {}, +} + +partners := object.get(input, "partners", []) if { true } +partners_count := object.get(input, "partners_count", 0) if { true } + +default_inbound_access_type := access_type if { + inbound := object.get(input, "b2b_collaboration_inbound", {}) + users_and_groups := object.get(inbound, "usersAndGroups", {}) + access_type := object.get(users_and_groups, "accessType", "") +} + +compliant_value := true if { + partners_count > 0 + default_inbound_access_type == "blocked" +} else := false if { true } + +msg := sprintf("Cross-tenant collaboration is restricted by default (partners=%d, accessType=%v)", [partners_count, default_inbound_access_type]) if { compliant_value } else := sprintf("Cross-tenant collaboration is not sufficiently restricted (partners=%d, accessType=%v)", [partners_count, default_inbound_access_type]) if { true } + +# Heuristic evaluation: +# - Consider compliant when the tenant has explicit partner configuration (partners_count > 0) +# and the default inbound B2B collaboration access is not wide-open. +result := output if { + output := { + "compliant": compliant_value, + "message": msg, + "details": { + "partners_count": partners_count, + "partner_tenant_ids": [p.tenantId | some p in partners; p.tenantId != null], + "default_inbound_access_type": default_inbound_access_type, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.6.2_restrict_guest_user_access.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.6.2_restrict_guest_user_access.rego new file mode 100644 index 00000000..78bd9f1f --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.6.2_restrict_guest_user_access.rego @@ -0,0 +1,54 @@ +# METADATA +# title: Ensure that guest user access is restricted +# description: Restrict guest user access permissions in Entra ID. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions +# description: Restrict guest access permissions +# custom: +# control_id: CIS-5.1.6.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_6_2 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine guestUserRoleId", + "details": {}, +} + +# Known guestUserRoleId values (Microsoft docs): +# - Same as members: a0b1b346-4d3e-4e8b-98f8-753987be4970 (NOT compliant) +# - Limited access (default): 10dae51f-b6af-4016-8d66-8c2a99b929b3 (compliant) +# - Most restrictive: 2af84b1e-32c8-42b7-82bc-daa82404023b (compliant) + +same_as_member := "a0b1b346-4d3e-4e8b-98f8-753987be4970" if { true } +limited := "10dae51f-b6af-4016-8d66-8c2a99b929b3" if { true } +restricted := "2af84b1e-32c8-42b7-82bc-daa82404023b" if { true } + +ok if { input.guest_user_role_id == limited } +ok if { input.guest_user_role_id == restricted } + +compliant_value := true if { ok } else := false if { true } + +msg := "Guest user access is restricted" if { ok } else := "Guest user access is NOT restricted (guests have member-like permissions)" if { input.guest_user_role_id == same_as_member } else := "Unable to determine guestUserRoleId" if { input.guest_user_role_id == null } else := sprintf("Guest user access is NOT restricted (guestUserRoleId=%v)", [input.guest_user_role_id]) if { true } + +result := out if { + role_id := input.guest_user_role_id + + out := { + "compliant": compliant_value, + "message": msg, + "details": { + "guest_user_role_id": role_id, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.6.3_limit_guest_invitations.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.6.3_limit_guest_invitations.rego new file mode 100644 index 00000000..8531436b --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.6.3_limit_guest_invitations.rego @@ -0,0 +1,44 @@ +# METADATA +# title: Ensure guest user invitations are limited to the Guest Inviter role +# description: Limit who can invite guest users into the tenant. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/graph/api/authorizationpolicy-get +# description: authorizationPolicy resource type +# custom: +# control_id: CIS-5.1.6.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_6_3 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine allowInvitesFrom", + "details": {}, +} + +compliant_value := true if { input.allow_invites_from == "adminsAndGuestInviters" } else := false if { true } + +msg := "Guest invitations are limited to admins and Guest Inviter role (allowInvitesFrom=adminsAndGuestInviters)" if { input.allow_invites_from == "adminsAndGuestInviters" } else := sprintf("Guest invitations are not sufficiently restricted (allowInvitesFrom=%v)", [input.allow_invites_from]) if { input.allow_invites_from != null; input.allow_invites_from != "adminsAndGuestInviters" } else := "Unable to determine allowInvitesFrom" if { input.allow_invites_from == null } else := "Unable to determine allowInvitesFrom" if { true } + +result := out if { + v := input.allow_invites_from + + # Expected: only admins and Guest Inviter role can invite + + out := { + "compliant": compliant_value, + "message": msg, + "details": { + "allow_invites_from": v, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.2.3_block_legacy_auth.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.2.3_block_legacy_auth.rego new file mode 100644 index 00000000..ffbd4367 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.2.3_block_legacy_auth.rego @@ -0,0 +1,72 @@ +# METADATA +# title: Enable Conditional Access policies to block legacy authentication +# description: | +# Legacy authentication protocols do not support MFA and are commonly exploited +# in password spray and credential stuffing attacks. Implement Conditional Access +# policies to block these protocols for all users and all applications. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-5.2.2.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_2_2_3 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve Conditional Access policy data", + "details": {} +} + +# Main evaluation rule +result := output if { + policies := get_array(input, "conditional_access_policies") + + blocking_policies := [p | some p in policies; is_legacy_auth_block_policy(p)] + compliant := count(blocking_policies) > 0 + + msg := build_message(compliant, blocking_policies) + affected := build_affected(compliant, blocking_policies) + + output := { + "compliant": compliant, + "message": msg, + "affected_resources": affected, + "details": { + "total_policies": count(policies), + "legacy_auth_block_policies": count(blocking_policies), + "blocking_policy_names": [p.display_name | some p in blocking_policies] + } + } +} + +# Helper to get array with default +get_array(obj, key) := value if { + value := obj[key] +} else := [] + +is_legacy_auth_block_policy(policy) if { + policy.state == "enabled" + policy.targets_all_users == true + policy.targets_all_apps == true + policy.blocks_legacy_auth == true + policy.grant_control == "block" +} + +build_message(true, blocking_policies) := msg if { + msg := sprintf("Found %d Conditional Access policy(ies) blocking legacy authentication", [count(blocking_policies)]) +} + +build_message(false, _) := "No Conditional Access policy found that blocks legacy authentication for all users and applications" + +build_affected(true, _) := [] +build_affected(false, _) := ["No legacy auth blocking policy configured"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.1_mfa_fatigue_protection.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.1_mfa_fatigue_protection.rego new file mode 100644 index 00000000..a27a6bc9 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.1_mfa_fatigue_protection.rego @@ -0,0 +1,49 @@ +# METADATA +# title: Ensure Microsoft Authenticator is configured to protect against MFA fatigue +# description: Configure Authenticator to prevent MFA fatigue attacks. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-number-match +# description: Number matching (conceptual) +# custom: +# control_id: CIS-5.2.3.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_2_3_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine Microsoft Authenticator MFA fatigue protection settings", + "details": {}, +} + +compliant_value := true if { input.number_matching_enabled == true } else := false if { true } + +msg := "MFA fatigue protection is enabled (number matching required)" if { input.number_matching_enabled == true } else := "MFA fatigue protection is not enabled (number matching not required)" if { input.number_matching_enabled != true } else := "Unable to determine Microsoft Authenticator MFA fatigue protection settings" if { true } + +result := output if { + number_matching := input.number_matching_enabled + app_ctx := input.display_app_information_enabled + loc_ctx := input.display_location_information_enabled + + output := { + "compliant": compliant_value, + "message": msg, + "details": { + "state": input.state, + "number_matching_enabled": number_matching, + "display_app_information_enabled": app_ctx, + "display_location_information_enabled": loc_ctx, + "include_targets_count": count(input.include_targets), + "exclude_targets_count": count(input.exclude_targets), + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.2_custom_banned_passwords_enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.2_custom_banned_passwords_enabled.rego new file mode 100644 index 00000000..b425bd3b --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.2_custom_banned_passwords_enabled.rego @@ -0,0 +1,46 @@ +# METADATA +# title: Ensure custom banned passwords lists are used +# description: Configure custom banned password list. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad +# description: Password protection and banned passwords (conceptual) +# custom: +# control_id: CIS-5.2.3.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Directory.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_2_3_2 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine custom banned password list configuration", + "details": {}, +} + +compliant_value := true if { input.banned_password_list_enabled == true } else := false if { true } + +msg := "Custom banned password list is enabled" if { input.banned_password_list_enabled == true } else := "Custom banned password list is not enabled" if { input.banned_password_list_enabled != true } else := "Unable to determine custom banned password list configuration" if { true } + +banned_list_present := true if { input.banned_password_list != null; input.banned_password_list != "" } else := false if { true } + +result := output if { + enabled := input.banned_password_list_enabled + list := input.banned_password_list + + output := { + "compliant": compliant_value, + "message": msg, + "details": { + "banned_password_list_enabled": enabled, + "banned_password_list_present": banned_list_present, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.3_enable_on_prem_password_protection.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.3_enable_on_prem_password_protection.rego new file mode 100644 index 00000000..daa9afb7 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.3_enable_on_prem_password_protection.rego @@ -0,0 +1,48 @@ +# METADATA +# title: Ensure password protection is enabled for on-prem Active Directory +# description: Enable password protection for on-premises AD. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad +# description: Password protection and banned passwords (conceptual) +# custom: +# control_id: CIS-5.2.3.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Directory.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_2_3_3 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine on-prem password protection configuration", + "details": {}, +} + +default compliant := false + +compliant if { + input.on_prem_protection_enabled == true +} + +msg := "On-prem password protection is enabled" if { compliant } +msg := "On-prem password protection is not enabled" if { not compliant } + +result := output if { + enabled := input.on_prem_protection_enabled + + output := { + "compliant": compliant, + "message": msg, + "details": { + "on_prem_protection_enabled": enabled, + "enforce_custom_banned_passwords": input.enforce_custom_banned_passwords, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.4_all_members_mfa_capable.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.4_all_members_mfa_capable.rego new file mode 100644 index 00000000..c7b5b6c9 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.4_all_members_mfa_capable.rego @@ -0,0 +1,53 @@ +# METADATA +# title: Ensure all member users are 'MFA capable' +# description: Ensure all users have registered MFA methods. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/graph/api/reportroot-list-authenticationmethods-userregistrationdetails +# description: Authentication methods user registration details report +# custom: +# control_id: CIS-5.2.3.4 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: EntraID +# requires_permissions: +# - AuditLog.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_2_3_4 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine MFA capability across users", + "details": {}, +} + +default compliant := false + +compliant if { + input.total_users > 0 + input.mfa_capable_count == input.total_users +} + +msg := "All users are MFA capable" if { compliant } +msg := sprintf("%d of %d users are MFA capable", [input.mfa_capable_count, input.total_users]) if { not compliant } + +result := output if { + total := input.total_users + capable := input.mfa_capable_count + + output := { + "compliant": compliant, + "message": msg, + "details": { + "total_users": total, + "mfa_capable_count": capable, + "mfa_registered_count": input.mfa_registered_count, + "mfa_not_registered_count": input.mfa_not_registered_count, + "mfa_registration_percentage": input.mfa_registration_percentage, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.5_disable_weak_auth_methods.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.5_disable_weak_auth_methods.rego new file mode 100644 index 00000000..5393c8e2 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.5_disable_weak_auth_methods.rego @@ -0,0 +1,50 @@ +# METADATA +# title: Ensure weak authentication methods are disabled +# description: Disable SMS and voice authentication methods. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/graph/api/resources/authenticationmethodspolicy +# description: Authentication methods policy +# custom: +# control_id: CIS-5.2.3.5 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_2_3_5 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine SMS/Voice authentication method status", + "details": {}, +} + +default compliant := false + +compliant if { + input.sms_enabled == false + input.voice_enabled == false +} + +msg := "Weak authentication methods (SMS, Voice) are disabled" if { compliant } +msg := sprintf("Weak authentication methods enabled (sms=%v, voice=%v)", [input.sms_enabled, input.voice_enabled]) if { not compliant } + +result := output if { + sms := input.sms_enabled + voice := input.voice_enabled + + output := { + "compliant": compliant, + "message": msg, + "details": { + "sms_enabled": sms, + "voice_enabled": voice, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.6_enable_system_preferred_mfa.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.6_enable_system_preferred_mfa.rego new file mode 100644 index 00000000..105b7c91 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.6_enable_system_preferred_mfa.rego @@ -0,0 +1,56 @@ +# METADATA +# title: Ensure system-preferred multifactor authentication is enabled +# description: Enable system-preferred MFA. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-system-preferred +# description: System-preferred MFA (conceptual) +# custom: +# control_id: CIS-5.2.3.6 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_2_3_6 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine system-preferred MFA configuration", + "details": {}, +} + +# Note: some tenants may not expose this setting; this remains best-effort. +default compliant := false + +compliant if { + policy := input.authentication_methods_policy + prefs := object.get(policy, "systemCredentialPreferences", {}) + object.get(prefs, "state", null) == "enabled" +} + +msg := "System-preferred MFA is enabled" if { compliant } +msg := sprintf( + "System-preferred MFA is not enabled (state=%v)", + [object.get(object.get(input.authentication_methods_policy, "systemCredentialPreferences", {}), "state", null)], +) if { not compliant } + +# Best-effort: check tenant-level setting on authenticationMethodsPolicy if present. +result := output if { + policy := input.authentication_methods_policy + prefs := object.get(policy, "systemCredentialPreferences", {}) + state := object.get(prefs, "state", null) + + output := { + "compliant": compliant, + "message": msg, + "details": { + "system_credential_preferences_state": state, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.7_disable_email_otp.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.7_disable_email_otp.rego new file mode 100644 index 00000000..7f2b930e --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.2.3.7_disable_email_otp.rego @@ -0,0 +1,46 @@ +# METADATA +# title: Ensure the email OTP authentication method is disabled +# description: Disable email OTP authentication. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/graph/api/resources/authenticationmethodspolicy +# description: Authentication methods policy +# custom: +# control_id: CIS-5.2.3.7 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_2_3_7 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine Email OTP authentication method status", + "details": {}, +} + +default compliant := false + +compliant if { input.email_otp_enabled == false } + +msg := "Email OTP authentication method is disabled" if { compliant } +msg := "Email OTP authentication method is enabled" if { input.email_otp_enabled == true } +msg := "Unable to determine Email OTP authentication method status" if { input.email_otp_enabled == null } + +result := output if { + email := input.email_otp_enabled + + output := { + "compliant": compliant, + "message": msg, + "details": { + "email_otp_enabled": email, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.1_pim_enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.1_pim_enabled.rego new file mode 100644 index 00000000..c541106e --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.1_pim_enabled.rego @@ -0,0 +1,45 @@ +# METADATA +# title: Ensure 'Privileged Identity Management' is used to manage roles +# description: Use PIM for privileged role management. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-configure +# description: Privileged Identity Management (conceptual) +# custom: +# control_id: CIS-5.3.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: EntraID +# requires_permissions: +# - RoleManagementPolicy.Read.Directory + +package cis.microsoft_365_foundations.v6_0_0.control_5_3_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine if PIM is enabled", + "details": {}, +} + +default compliant := false + +compliant if { input.pim_enabled == true } + +msg := "PIM is enabled (role management policies present)" if { compliant } +msg := "PIM does not appear to be enabled (no role management policies found)" if { not compliant } + +result := output if { + enabled := input.pim_enabled + + output := { + "compliant": compliant, + "message": msg, + "details": { + "total_policies": input.total_policies, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.2_guest_access_reviews_configured.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.2_guest_access_reviews_configured.rego new file mode 100644 index 00000000..3cdf3751 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.2_guest_access_reviews_configured.rego @@ -0,0 +1,46 @@ +# METADATA +# title: Ensure 'Access reviews' for Guest Users are configured +# description: Configure access reviews for guest users. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/graph/api/resources/accessreviewset +# description: Access reviews +# custom: +# control_id: CIS-5.3.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - AccessReview.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_3_2 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine guest access reviews", + "details": {}, +} + +default compliant := false + +compliant if { input.has_guest_reviews == true } + +msg := "Guest user access reviews are configured" if { compliant } +msg := "No guest user access reviews are configured" if { not compliant } + +result := output if { + has := input.has_guest_reviews + + output := { + "compliant": compliant, + "message": msg, + "details": { + "total_reviews": input.total_reviews, + "guest_reviews_count": input.guest_reviews_count, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.3_privileged_role_access_reviews_configured.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.3_privileged_role_access_reviews_configured.rego new file mode 100644 index 00000000..ebb0cbd4 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.3_privileged_role_access_reviews_configured.rego @@ -0,0 +1,67 @@ +# METADATA +# title: Ensure 'Access reviews' for privileged roles are configured +# description: Configure access reviews for privileged roles. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/graph/api/resources/accessreviewset +# description: Access reviews +# custom: +# control_id: CIS-5.3.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: EntraID +# requires_permissions: +# - AccessReview.Read.All + +package cis.microsoft_365_foundations.v6_0_0.control_5_3_3 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine access reviews for privileged roles", + "details": {}, +} + +is_role_query(q) if { contains(q, "role") } +is_role_query(q) if { contains(q, "directoryrole") } + +# Best-effort: compliant if any access review definition query references role-based scope. +default compliant := false + +compliant if { + some d in input.access_review_definitions + scope := d.scope + q := lower(scope.query) + is_role_query(q) +} + +msg := sprintf( + "Found %d access review definition(s) that appear to target roles", + [count([d | some d in input.access_review_definitions; is_role_query(lower(d.scope.query))])], +) if { compliant } + +msg := "No access review definitions appear to target privileged roles" if { not compliant } + +# Best-effort: treat as compliant if any access review definition query references role-based scope. +result := output if { + defs := input.access_review_definitions + role_reviews := [d | + some d in defs + scope := d.scope + q := lower(scope.query) + is_role_query(q) + ] + + output := { + "compliant": compliant, + "message": msg, + "details": { + "total_reviews": input.total_reviews, + "role_reviews_count": count(role_reviews), + "sample_role_review_ids": [d.id | some d in role_reviews], + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.4_ga_activation_requires_approval.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.4_ga_activation_requires_approval.rego new file mode 100644 index 00000000..7802ae86 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.4_ga_activation_requires_approval.rego @@ -0,0 +1,50 @@ +# METADATA +# title: Ensure approval is required for Global Administrator role activation +# description: Require approval for Global Admin role activation. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-role-activation +# description: PIM role activation settings (conceptual) +# custom: +# control_id: CIS-5.3.4 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: critical +# service: EntraID +# requires_permissions: +# - RoleManagementPolicy.Read.Directory + +package cis.microsoft_365_foundations.v6_0_0.control_5_3_4 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine Global Administrator approval requirements", + "details": {}, +} + +default compliant := false + +compliant if { input.global_admin_approval_required == true } + +msg := "Approval is required for Global Administrator activation" if { compliant } +msg := "Approval is NOT required for Global Administrator activation" if { input.global_admin_approval_required == false } +msg := "Unable to determine Global Administrator approval requirements" if { input.global_admin_approval_required == null } + +result := output if { + required := input.global_admin_approval_required + + output := { + "compliant": compliant, + "message": msg, + "details": { + "global_admin_policy": input.global_admin_policy, + "approval_required": required, + "mfa_required": input.global_admin_mfa_required, + "justification_required": input.global_admin_justification_required, + "max_activation_duration": input.global_admin_max_activation_duration, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.5_pra_activation_requires_approval.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.5_pra_activation_requires_approval.rego new file mode 100644 index 00000000..386cdc79 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.3.5_pra_activation_requires_approval.rego @@ -0,0 +1,47 @@ +# METADATA +# title: Ensure approval is required for Privileged Role Administrator activation +# description: Require approval for Privileged Role Admin activation. +# related_resources: +# - ref: https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-role-activation +# description: PIM role activation settings (conceptual) +# custom: +# control_id: CIS-5.3.5 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: critical +# service: EntraID +# requires_permissions: +# - RoleManagementPolicy.Read.Directory + +package cis.microsoft_365_foundations.v6_0_0.control_5_3_5 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Unable to determine Privileged Role Administrator approval requirements", + "details": {}, +} + +default compliant := false + +compliant if { input.privileged_role_admin_approval_required == true } + +msg := "Approval is required for Privileged Role Administrator activation" if { compliant } +msg := "Approval is NOT required for Privileged Role Administrator activation" if { input.privileged_role_admin_approval_required == false } +msg := "Unable to determine Privileged Role Administrator approval requirements" if { input.privileged_role_admin_approval_required == null } + +result := output if { + required := input.privileged_role_admin_approval_required + + output := { + "compliant": compliant, + "message": msg, + "details": { + "privileged_role_admin_policy": input.privileged_role_admin_policy, + "approval_required": required, + }, + } +} + diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.1.1_audit_disabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.1.1_audit_disabled.rego new file mode 100644 index 00000000..4cec9c49 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.1.1_audit_disabled.rego @@ -0,0 +1,56 @@ +# METADATA +# title: Ensure 'AuditDisabled' organizationally is set to 'False' +# description: | +# Mailbox auditing is enabled by default for all organizations. Ensure that +# organization-wide auditing has not been explicitly disabled, as this would +# prevent the capture of critical mailbox activities for security investigations. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.1.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_1_1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + audit_disabled := input.audit_disabled + + # Compliant when AuditDisabled is False (auditing is enabled) + compliant := audit_disabled == false + + output := { + "compliant": compliant, + "message": generate_message(audit_disabled), + "affected_resources": generate_affected_resources(compliant), + "details": { + "audit_disabled": audit_disabled + } + } +} + +generate_message(audit_disabled) := msg if { + audit_disabled == false + msg := "Organization-wide mailbox auditing is enabled (AuditDisabled = False)" +} + +generate_message(audit_disabled) := msg if { + audit_disabled == true + msg := "Organization-wide mailbox auditing is disabled (AuditDisabled = True)" +} + +generate_message(audit_disabled) := msg if { + audit_disabled == null + msg := "Unable to determine AuditDisabled status" +} + +generate_affected_resources(true) := [] +generate_affected_resources(false) := ["Organization mailbox auditing is disabled"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.1.2_mailbox_audit_actions.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.1.2_mailbox_audit_actions.rego new file mode 100644 index 00000000..cd4ad991 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.1.2_mailbox_audit_actions.rego @@ -0,0 +1,69 @@ +# METADATA +# title: Ensure mailbox audit actions are configured +# description: | +# Mailbox auditing should capture comprehensive audit actions for Admin, +# Delegate, and Owner operations. The default audit actions may not cover +# all necessary activities for security investigations and compliance. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.1.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_1_2 + +default result := {"compliant": false, "message": "Evaluation failed"} + +# Required audit actions per CIS benchmark +required_admin_actions := {"ApplyRecord", "Copy", "Create", "FolderBind", "HardDelete", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules"} +required_delegate_actions := {"ApplyRecord", "Create", "FolderBind", "HardDelete", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateFolderPermissions", "UpdateInboxRules"} +required_owner_actions := {"ApplyRecord", "Create", "HardDelete", "MailboxLogin", "Move", "MoveToDeletedItems", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules"} + +# Check if a mailbox has required audit actions +mailbox_has_required_actions(mailbox) if { + admin_actions := {a | some a in mailbox.AuditAdmin} + delegate_actions := {a | some a in mailbox.AuditDelegate} + owner_actions := {a | some a in mailbox.AuditOwner} + + count(required_admin_actions - admin_actions) == 0 + count(required_delegate_actions - delegate_actions) == 0 + count(required_owner_actions - owner_actions) == 0 +} + +result := output if { + mailboxes := input.mailboxes + total := count(mailboxes) + + # Find non-compliant mailboxes + non_compliant := [m.UserPrincipalName | some m in mailboxes; not mailbox_has_required_actions(m)] + + compliant := count(non_compliant) == 0 + + output := { + "compliant": compliant, + "message": generate_message(total, non_compliant), + "affected_resources": non_compliant, + "details": { + "total_mailboxes": total, + "compliant_mailboxes": total - count(non_compliant), + "non_compliant_mailboxes": count(non_compliant) + } + } +} + +generate_message(total, non_compliant) := msg if { + count(non_compliant) == 0 + msg := sprintf("All %d mailbox(es) have required audit actions configured", [total]) +} + +generate_message(total, non_compliant) := msg if { + count(non_compliant) > 0 + msg := sprintf("%d of %d mailbox(es) are missing required audit actions", [count(non_compliant), total]) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.1.3_audit_bypass.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.1.3_audit_bypass.rego new file mode 100644 index 00000000..2e99d6a7 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.1.3_audit_bypass.rego @@ -0,0 +1,50 @@ +# METADATA +# title: Ensure 'AuditBypassEnabled' is not enabled on mailboxes +# description: | +# Mailbox audit bypass allows specified accounts to perform actions without +# generating audit entries. No mailboxes should have AuditBypassEnabled set +# to True, as this creates blind spots in security monitoring. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.1.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_1_3 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + accounts_with_bypass := input.accounts_with_bypass_enabled + bypass_count := input.bypass_count + + # Compliant when no accounts have audit bypass enabled + compliant := bypass_count == 0 + + output := { + "compliant": compliant, + "message": generate_message(bypass_count), + "affected_resources": [a.Name | some a in accounts_with_bypass], + "details": { + "accounts_with_bypass_enabled": bypass_count, + "bypassed_accounts": [a.Name | some a in accounts_with_bypass] + } + } +} + +generate_message(bypass_count) := msg if { + bypass_count == 0 + msg := "No accounts have mailbox audit bypass enabled" +} + +generate_message(bypass_count) := msg if { + bypass_count > 0 + msg := sprintf("%d account(s) have mailbox audit bypass enabled", [bypass_count]) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.2.1_mail_forwarding_blocked.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.2.1_mail_forwarding_blocked.rego new file mode 100644 index 00000000..0d082a2b --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.2.1_mail_forwarding_blocked.rego @@ -0,0 +1,129 @@ +# METADATA +# title: Ensure all forms of mail forwarding are blocked and/or disabled +# description: | +# Transport rules that automatically forward or redirect mail to external +# recipients pose a significant data exfiltration risk. Additionally, +# outbound spam filter policies must have AutoForwardingMode set to Off. +# Rules that whitelist domains by setting SCL to -1 also pose security risks. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.2.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_2_1 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve outbound spam filter policy data", + "details": {} +} + +# Main evaluation rule +result := output if { + # Outbound policies are required + outbound_policies := input.outbound_spam_filter_policies + auto_forwarding_blocked := input.auto_forwarding_blocked + + # Get rules (default to empty array if missing) + forwarding_rules := get_array(input, "forwarding_rules") + whitelist_rules := get_array(input, "whitelist_rules") + + # Filter to only enabled rules + enabled_forwarding_rules := [r | some r in forwarding_rules; r.state == "Enabled"] + enabled_whitelist_rules := [r | some r in whitelist_rules; r.state == "Enabled"] + + # Find policies where AutoForwardingMode is not Off + non_compliant_policies := [p | some p in outbound_policies; p.auto_forwarding_mode != "Off"] + + # Check all conditions - compliant only if all are true + compliant := is_compliant(enabled_forwarding_rules, enabled_whitelist_rules, auto_forwarding_blocked) + + # Build message + msg := build_message(enabled_forwarding_rules, enabled_whitelist_rules, non_compliant_policies, auto_forwarding_blocked) + + # Build affected resources + affected := build_affected(enabled_forwarding_rules, enabled_whitelist_rules, non_compliant_policies) + + output := { + "compliant": compliant, + "message": msg, + "affected_resources": affected, + "details": { + "total_transport_rules": get_number(input, "total_rules"), + "enabled_forwarding_rules": count(enabled_forwarding_rules), + "enabled_whitelist_rules": count(enabled_whitelist_rules), + "forwarding_rules": enabled_forwarding_rules, + "whitelist_rules": enabled_whitelist_rules, + "outbound_spam_filter_policies": outbound_policies, + "auto_forwarding_blocked": auto_forwarding_blocked + } + } +} + +# Helper to get array with default +get_array(obj, key) := value if { + value := obj[key] +} else := [] + +# Helper to get number with default +get_number(obj, key) := value if { + value := obj[key] +} else := 0 + +# Helper to compute compliance - true only if all conditions met +is_compliant(fwd_rules, wl_rules, blocked) := true if { + count(fwd_rules) == 0 + count(wl_rules) == 0 + blocked == true +} else := false + +# Build message - all compliant +build_message(fwd, wl, policies, blocked) := "All forms of mail forwarding are blocked and no domain whitelist rules exist" if { + count(fwd) == 0 + count(wl) == 0 + blocked == true +} + +# Build message - has issues +build_message(fwd, wl, policies, blocked) := msg if { + issues := array.concat( + array.concat( + fwd_issues(fwd), + wl_issues(wl) + ), + policy_issues(policies, blocked) + ) + count(issues) > 0 + msg := concat("; ", issues) +} + +# Issue helpers +fwd_issues(rules) := [sprintf("%d enabled forwarding rule(s)", [count(rules)])] if { + count(rules) > 0 +} else := [] + +wl_issues(rules) := [sprintf("%d enabled whitelist rule(s) with SCL=-1", [count(rules)])] if { + count(rules) > 0 +} else := [] + +policy_issues(policies, blocked) := [sprintf("AutoForwardingMode not Off in %d policy(ies)", [count(policies)])] if { + blocked == false +} else := [] + +# Build affected resources +build_affected(fwd, wl, policies) := resources if { + fwd_names := [sprintf("Forwarding rule: %s", [r.name]) | some r in fwd] + wl_names := [sprintf("Whitelist rule: %s (domains: %v)", [r.name, r.sender_domain]) | some r in wl] + policy_names := [sprintf("Outbound policy '%s' has AutoForwardingMode=%s", [p.name, p.auto_forwarding_mode]) | some p in policies] + resources := array.concat(array.concat(fwd_names, wl_names), policy_names) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.2.2_transport_whitelist.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.2.2_transport_whitelist.rego new file mode 100644 index 00000000..c65741b9 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.2.2_transport_whitelist.rego @@ -0,0 +1,58 @@ +# METADATA +# title: Ensure mail transport rules do not whitelist specific domains +# description: | +# Transport rules that whitelist specific domains by setting SCL to -1 or +# bypassing spam filtering based on sender domain create security risks. +# These rules can be exploited by attackers using spoofed domains. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.2.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_2_2 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + whitelist_rules := input.whitelist_rules + + # Filter to only enabled whitelist rules that set SCL to -1 + enabled_whitelist_rules := [r | + some r in whitelist_rules + r.state == "Enabled" + r.set_scl == -1 + ] + + # Compliant when no enabled whitelist rules exist + compliant := count(enabled_whitelist_rules) == 0 + + output := { + "compliant": compliant, + "message": generate_message(enabled_whitelist_rules), + "affected_resources": [r.name | some r in enabled_whitelist_rules], + "details": { + "total_transport_rules": input.total_rules, + "whitelist_rules_count": count(whitelist_rules), + "enabled_whitelist_rules": count(enabled_whitelist_rules), + "whitelist_rules": [{"name": r.name, "domains": r.sender_domain} | some r in enabled_whitelist_rules] + } + } +} + +generate_message(enabled_whitelist_rules) := msg if { + count(enabled_whitelist_rules) == 0 + msg := "No enabled transport rules whitelist specific domains" +} + +generate_message(enabled_whitelist_rules) := msg if { + count(enabled_whitelist_rules) > 0 + msg := sprintf("%d enabled transport rule(s) whitelist specific domains (SCL = -1)", [count(enabled_whitelist_rules)]) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.2.3_external_sender_tagging.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.2.3_external_sender_tagging.rego new file mode 100644 index 00000000..3d7c856f --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.2.3_external_sender_tagging.rego @@ -0,0 +1,66 @@ +# METADATA +# title: Ensure email from external senders is identified +# description: | +# External sender identification helps users identify potentially malicious +# emails from outside the organization. This setting adds visual indicators +# to emails received from external sources. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.2.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_2_3 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + enabled := input.enabled + allowed_senders := input.allowed_senders + + # Compliant when external tagging is enabled + compliant := enabled == true + + output := { + "compliant": compliant, + "message": generate_message(enabled, allowed_senders), + "affected_resources": generate_affected_resources(compliant), + "details": { + "external_tagging_enabled": enabled, + "allowed_senders_count": count(allowed_senders), + "allowed_senders": allowed_senders + } + } +} + +generate_message(enabled, allowed_senders) := msg if { + enabled == true + count(allowed_senders) == 0 + msg := "External sender tagging is enabled with no exceptions" +} + +generate_message(enabled, allowed_senders) := msg if { + enabled == true + count(allowed_senders) > 0 + msg := sprintf("External sender tagging is enabled with %d exception(s)", [count(allowed_senders)]) +} + +generate_message(enabled, _) := msg if { + enabled == false + msg := "External sender tagging is disabled" +} + +generate_message(enabled, _) := msg if { + enabled == null + msg := "Unable to determine external sender tagging status" +} + +generate_affected_resources(true) := [] +generate_affected_resources(false) := ["External sender tagging is not enabled"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.3.1_outlook_addins.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.3.1_outlook_addins.rego new file mode 100644 index 00000000..3a0a6ee6 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.3.1_outlook_addins.rego @@ -0,0 +1,50 @@ +# METADATA +# title: Ensure users installing Outlook add-ins is not allowed +# description: | +# User installation of Outlook add-ins should be restricted to prevent +# potentially malicious add-ins from accessing mailbox data. Add-in +# installation should be centrally managed by administrators. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.3.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_3_1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + policies_allowing_addins := input.policies_allowing_addin_install + + # Compliant when no policies allow add-in installation + compliant := count(policies_allowing_addins) == 0 + + output := { + "compliant": compliant, + "message": generate_message(policies_allowing_addins), + "affected_resources": [p.name | some p in policies_allowing_addins], + "details": { + "total_policies": input.total_policies, + "policies_allowing_addins": count(policies_allowing_addins), + "policy_details": policies_allowing_addins + } + } +} + +generate_message(policies_allowing_addins) := msg if { + count(policies_allowing_addins) == 0 + msg := "No role assignment policies allow user add-in installation" +} + +generate_message(policies_allowing_addins) := msg if { + count(policies_allowing_addins) > 0 + msg := sprintf("%d role assignment policy(ies) allow user add-in installation", [count(policies_allowing_addins)]) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.1_modern_auth.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.1_modern_auth.rego new file mode 100644 index 00000000..2be77f66 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.1_modern_auth.rego @@ -0,0 +1,56 @@ +# METADATA +# title: Ensure modern authentication for Exchange Online is enabled +# description: | +# Modern authentication (OAuth 2.0) provides enhanced security features +# including MFA support and conditional access. OAuth2ClientProfileEnabled +# must be set to True for Exchange Online. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.5.1 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: high +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_5_1 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + oauth_enabled := input.oauth_enabled + + # Compliant when OAuth authentication is enabled + compliant := oauth_enabled == true + + output := { + "compliant": compliant, + "message": generate_message(oauth_enabled), + "affected_resources": generate_affected_resources(compliant), + "details": { + "oauth2_client_profile_enabled": oauth_enabled + } + } +} + +generate_message(oauth_enabled) := msg if { + oauth_enabled == true + msg := "Modern authentication (OAuth 2.0) is enabled for Exchange Online" +} + +generate_message(oauth_enabled) := msg if { + oauth_enabled == false + msg := "Modern authentication (OAuth 2.0) is disabled for Exchange Online" +} + +generate_message(oauth_enabled) := msg if { + oauth_enabled == null + msg := "Unable to determine modern authentication status" +} + +generate_affected_resources(true) := [] +generate_affected_resources(false) := ["Modern authentication is disabled"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.2_mailtips.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.2_mailtips.rego new file mode 100644 index 00000000..cf844238 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.2_mailtips.rego @@ -0,0 +1,94 @@ +# METADATA +# title: Ensure MailTips are enabled for end users +# description: | +# MailTips provide informational messages to users as they compose emails, +# helping prevent accidental data disclosure and improving email hygiene. +# Key settings include tips for external recipients and large audiences. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.5.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: low +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_5_2 + +default result := {"compliant": false, "message": "Evaluation failed"} + +# Helper to check if all required settings are properly configured +all_settings_compliant(all_tips, external_tips, group_metrics, threshold) if { + all_tips == true + external_tips == true + group_metrics == true + threshold != null + threshold > 0 +} + +result := output if { + config := input.organization_config + + all_tips_enabled := config.MailTipsAllTipsEnabled + external_tips_enabled := config.MailTipsExternalRecipientsTipsEnabled + group_metrics_enabled := config.MailTipsGroupMetricsEnabled + large_audience_threshold := config.MailTipsLargeAudienceThreshold + + # Compliant when all MailTips settings are properly configured + compliant := all_settings_compliant(all_tips_enabled, external_tips_enabled, group_metrics_enabled, large_audience_threshold) + + output := { + "compliant": compliant, + "message": generate_message(all_tips_enabled, external_tips_enabled, group_metrics_enabled), + "affected_resources": generate_affected_resources(all_tips_enabled, external_tips_enabled, group_metrics_enabled), + "details": { + "mail_tips_all_tips_enabled": all_tips_enabled, + "mail_tips_external_recipients_enabled": external_tips_enabled, + "mail_tips_group_metrics_enabled": group_metrics_enabled, + "mail_tips_large_audience_threshold": large_audience_threshold + } + } +} + +generate_message(all_tips, external_tips, group_metrics) := msg if { + all_tips == true + external_tips == true + group_metrics == true + msg := "MailTips are properly configured for end users" +} + +generate_message(all_tips, external_tips, group_metrics) := msg if { + not settings_all_true(all_tips, external_tips, group_metrics) + msg := "MailTips are not fully configured for end users" +} + +settings_all_true(all_tips, external_tips, group_metrics) if { + all_tips == true + external_tips == true + group_metrics == true +} + +generate_affected_resources(all_tips, external_tips, group_metrics) := resources if { + all_tips == true + external_tips == true + group_metrics == true + resources := [] +} + +generate_affected_resources(all_tips, external_tips, group_metrics) := resources if { + not settings_all_true(all_tips, external_tips, group_metrics) + resources := array.concat( + array.concat( + conditional_resource(all_tips != true, "MailTipsAllTipsEnabled is disabled"), + conditional_resource(external_tips != true, "MailTipsExternalRecipientsTipsEnabled is disabled") + ), + conditional_resource(group_metrics != true, "MailTipsGroupMetricsEnabled is disabled") + ) +} + +conditional_resource(true, msg) := [msg] +conditional_resource(false, _) := [] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.3_storage_providers.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.3_storage_providers.rego new file mode 100644 index 00000000..aea99dd1 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.3_storage_providers.rego @@ -0,0 +1,51 @@ +# METADATA +# title: Ensure additional storage providers are restricted in Outlook on the web +# description: | +# Additional storage providers (Dropbox, Google Drive, Box, etc.) in Outlook +# on the web can lead to data leakage if not properly controlled. Restrict +# third-party storage providers to maintain data within corporate boundaries. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.5.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_5_3 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + policies_with_external_storage := input.policies_with_external_storage + total_policies := input.total_policies + + # Compliant when no policies allow external storage providers + compliant := count(policies_with_external_storage) == 0 + + output := { + "compliant": compliant, + "message": generate_message(policies_with_external_storage, total_policies), + "affected_resources": policies_with_external_storage, + "details": { + "total_owa_policies": total_policies, + "policies_with_external_storage": count(policies_with_external_storage), + "policy_names": policies_with_external_storage + } + } +} + +generate_message(policies_with_storage, total) := msg if { + count(policies_with_storage) == 0 + msg := sprintf("All %d OWA mailbox policy(ies) restrict additional storage providers", [total]) +} + +generate_message(policies_with_storage, total) := msg if { + count(policies_with_storage) > 0 + msg := sprintf("%d of %d OWA mailbox policy(ies) allow additional storage providers", [count(policies_with_storage), total]) +} diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.4_smtp_auth.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.4_smtp_auth.rego new file mode 100644 index 00000000..481723e6 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.4_smtp_auth.rego @@ -0,0 +1,56 @@ +# METADATA +# title: Ensure SMTP AUTH is disabled +# description: | +# SMTP AUTH allows clients to submit email using basic authentication, +# which is vulnerable to credential theft attacks. Disable SMTP AUTH +# at the organization level to enforce modern authentication methods. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.5.4 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_5_4 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + smtp_auth_disabled := input.smtp_client_authentication_disabled + + # Compliant when SMTP client authentication is disabled + compliant := smtp_auth_disabled == true + + output := { + "compliant": compliant, + "message": generate_message(smtp_auth_disabled), + "affected_resources": generate_affected_resources(compliant), + "details": { + "smtp_client_authentication_disabled": smtp_auth_disabled + } + } +} + +generate_message(smtp_auth_disabled) := msg if { + smtp_auth_disabled == true + msg := "SMTP AUTH is disabled at the organization level" +} + +generate_message(smtp_auth_disabled) := msg if { + smtp_auth_disabled == false + msg := "SMTP AUTH is enabled at the organization level" +} + +generate_message(smtp_auth_disabled) := msg if { + smtp_auth_disabled == null + msg := "Unable to determine SMTP AUTH status" +} + +generate_affected_resources(true) := [] +generate_affected_resources(false) := ["SMTP AUTH is enabled"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.5_direct_send.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.5_direct_send.rego new file mode 100644 index 00000000..f538f453 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/6.5.5_direct_send.rego @@ -0,0 +1,56 @@ +# METADATA +# title: Ensure Direct Send submissions are rejected +# description: | +# Direct Send allows anonymous SMTP connections to send email as the +# organization. This can be exploited for phishing and spoofing attacks. +# Configure transport settings to reject unauthenticated direct send. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-6.5.5 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: Exchange +# requires_permissions: +# - Exchange.Manage + +package cis.microsoft_365_foundations.v6_0_0.control_6_5_5 + +default result := {"compliant": false, "message": "Evaluation failed"} + +result := output if { + reject_direct_send := input.reject_direct_send + + # Compliant when RejectDirectSend is true + compliant := reject_direct_send == true + + output := { + "compliant": compliant, + "message": generate_message(reject_direct_send), + "affected_resources": generate_affected_resources(compliant), + "details": { + "reject_direct_send": reject_direct_send + } + } +} + +generate_message(reject_direct_send) := msg if { + reject_direct_send == true + msg := "Direct Send submissions are rejected" +} + +generate_message(reject_direct_send) := msg if { + reject_direct_send == false + msg := "Direct Send submissions are allowed (RejectDirectSend is False)" +} + +generate_message(reject_direct_send) := msg if { + reject_direct_send == null + msg := "Unable to determine Direct Send status" +} + +generate_affected_resources(true) := [] +generate_affected_resources(false) := ["Direct Send submissions are allowed"] diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json b/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json new file mode 100644 index 00000000..6f611a82 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json @@ -0,0 +1,2111 @@ +{ + "framework": "cis", + "benchmark": "CIS Microsoft 365 Foundations", + "slug": "microsoft-365-foundations", + "version": "v6.0.0", + "release_date": "2025-03-18", + "platform": "m365", + "source_url": "https://www.cisecurity.org/benchmark/microsoft_365", + "controls": [ + { + "control_id": "1.1.1", + "title": "Ensure Administrative accounts are cloud-only", + "description": "Administrative accounts should not be synced from on-premises Active Directory.", + "severity": "critical", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.roles.cloud_only_admins", + "policy_file": "1.1.1_admin_cloud_only.rego", + "requires_permissions": ["User.Read.All", "RoleManagement.Read.Directory"], + "notes": null + }, + { + "control_id": "1.1.2", + "title": "Ensure two emergency access accounts have been defined", + "description": "Emergency access accounts provide access when normal administrative accounts are unavailable.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Organizational policy; requires human designation of accounts" + }, + { + "control_id": "1.1.3", + "title": "Ensure that between two and four global admins are designated", + "description": "Maintain between 2-4 global administrators to ensure operational continuity while minimizing attack surface.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.roles.privileged_roles", + "policy_file": "1.1.3_global_admin_count.rego", + "requires_permissions": ["RoleManagement.Read.Directory", "User.Read.All"], + "notes": null + }, + { + "control_id": "1.1.4", + "title": "Ensure administrative accounts use licenses with a reduced application footprint", + "description": "Administrative accounts should have minimal license assignments.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Need to check user license assignments" + }, + { + "control_id": "1.2.1", + "title": "Ensure that only organizationally managed/approved public groups exist", + "description": "Public groups should be reviewed and managed by the organization.", + "severity": "medium", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.groups.groups", + "policy_file": "1.2.1_no_unmanaged_public_groups.rego", + "requires_permissions": ["Group.Read.All"], + "notes": "Collector exists but control logic not defined" + }, + { + "control_id": "1.2.2", + "title": "Ensure sign-in to shared mailboxes is blocked", + "description": "Shared mailboxes should not allow direct sign-in.", + "severity": "medium", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "exchange.mailbox.mailboxes", + "policy_file": null, + "requires_permissions": ["Exchange.Manage"], + "notes": "Collector exists but control logic not defined" + }, + { + "control_id": "1.3.1", + "title": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'", + "description": "Configure password policies to never expire when MFA is enabled.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.domains.password_policy", + "policy_file": "1.3.1_password_expiration.rego", + "requires_permissions": ["Domain.Read.All"], + "notes": null + }, + { + "control_id": "1.3.2", + "title": "Ensure 'Idle session timeout' is set to '3 hours (or less)' for unmanaged devices", + "description": "Configure idle session timeout for unmanaged devices.", + "severity": "medium", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires CA policy coverage verification" + }, + { + "control_id": "1.3.3", + "title": "Ensure 'External sharing' of calendars is not available", + "description": "Disable external calendar sharing.", + "severity": "medium", + "service": "Exchange", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "exchange.organization.sharing_policy", + "policy_file": null, + "requires_permissions": ["Exchange.Manage"], + "notes": "Collector exists but control logic not defined" + }, + { + "control_id": "1.3.4", + "title": "Ensure 'User owned apps and services' is restricted", + "description": "Restrict user ability to consent to apps and services.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.applications.apps_and_services_settings", + "policy_file": "1.3.4_user_owned_apps_restricted.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "1.3.5", + "title": "Ensure internal phishing protection for Forms is enabled", + "description": "Enable internal phishing protection in Microsoft Forms.", + "severity": "medium", + "service": "Forms", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.applications.forms_settings", + "policy_file": "1.3.5_forms_internal_phishing_protection.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "1.3.6", + "title": "Ensure the customer lockbox feature is enabled", + "description": "Enable Customer Lockbox for Microsoft support access.", + "severity": "medium", + "service": "Exchange", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "exchange.organization.organization_config", + "policy_file": null, + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "1.3.7", + "title": "Ensure 'third-party storage services' are restricted in 'Microsoft 365 on the web'", + "description": "Restrict third-party storage providers in Office web apps.", + "severity": "medium", + "service": "Exchange", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "exchange.organization.owa_mailbox_policy", + "policy_file": null, + "requires_permissions": ["Exchange.Manage"], + "notes": "Collector exists but control logic not defined" + }, + { + "control_id": "1.3.8", + "title": "Ensure that Sways cannot be shared with people outside of your organization", + "description": "Restrict external sharing of Sway content.", + "severity": "medium", + "service": "Sway", + "level": "L2", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "No API available for Sway settings" + }, + { + "control_id": "1.3.9", + "title": "Ensure shared bookings pages are restricted to select users", + "description": "Restrict who can create and manage shared bookings pages.", + "severity": "medium", + "service": "Bookings", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Need Bookings API collector" + }, + { + "control_id": "2.1.1", + "title": "Ensure Safe Links for Office Applications is Enabled", + "description": "Enable Safe Links to protect users from malicious URLs.", + "severity": "high", + "service": "Defender", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.safe_links_policy", + "policy_file": "2.1.1_SafeLinks_OfficeApplications_Enabled.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.2", + "title": "Ensure the Common Attachment Types Filter is enabled", + "description": "Block common malicious attachment types.", + "severity": "high", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.malware_filter_policy", + "policy_file": "2.1.2_Common_AttachmentTypes_Enabled.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.3", + "title": "Ensure notifications for internal users sending malware is Enabled", + "description": "Notify administrators when internal users send malware.", + "severity": "medium", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.malware_filter_policy", + "policy_file": "2.1.3_Notifications_InternalUsers_sendingMalware_Enabled.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.4", + "title": "Ensure Safe Attachments policy is enabled", + "description": "Enable Safe Attachments to scan attachments for malware.", + "severity": "high", + "service": "Defender", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.safe_attachment_policy", + "policy_file": "2.1.4_Safe_AttachementsPolicy_Enabled.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.5", + "title": "Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled", + "description": "Enable ATP protection for files in SharePoint, OneDrive, and Teams.", + "severity": "high", + "service": "Defender", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.atp_policy_o365", + "policy_file": "2.1.5_Safe_Attachments_SharePoint_OneDrive_MSTeams_Enabled.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.6", + "title": "Ensure Exchange Online Spam Policies are set to notify administrators", + "description": "Configure spam policies to notify administrators.", + "severity": "medium", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.hosted_outbound_spam_filter", + "policy_file": "2.1.6_Exchange_OnlineSpam_Policies_Notify_Administrators.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.7", + "title": "Ensure that an anti-phishing policy has been created", + "description": "Create and configure anti-phishing policies.", + "severity": "high", + "service": "Defender", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.anti_phish_policy", + "policy_file": "2.1.7_AntiPhishing_Policy_is_created.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.8", + "title": "Ensure that SPF records are published for all Exchange Domains", + "description": "Publish SPF records to prevent email spoofing.", + "severity": "high", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.dns.dns_security_records", + "policy_file": "2.1.8_SPF_records_published.rego", + "requires_permissions": ["Domain.Read.All"], + "notes": "DNS lookup for SPF TXT records" + }, + { + "control_id": "2.1.9", + "title": "Ensure that DKIM is enabled for all Exchange Online Domains", + "description": "Enable DKIM signing for email authentication.", + "severity": "high", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.authentication.dkim_signing_config", + "policy_file": "2.1.9_DKIM_is_enabled.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.10", + "title": "Ensure DMARC Records for all Exchange Online domains are published", + "description": "Publish DMARC records for email authentication.", + "severity": "high", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.dns.dns_security_records", + "policy_file": "2.1.10_DMARC_records_published.rego", + "requires_permissions": ["Domain.Read.All"], + "notes": "DNS lookup for DMARC TXT records" + }, + { + "control_id": "2.1.11", + "title": "Ensure comprehensive attachment filtering is applied", + "description": "Configure comprehensive attachment type filtering.", + "severity": "medium", + "service": "Defender", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.malware_filter_policy", + "policy_file": "2.1.11_Comprehensive_Attachment_Filtering_Applied.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.12", + "title": "Ensure the connection filter IP allow list is not used", + "description": "Do not whitelist IPs in connection filter.", + "severity": "medium", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.hosted_connection_filter", + "policy_file": "2.1.12_ConnectionFilter_IPAllowList_not_used.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.13", + "title": "Ensure the connection filter safe list is off", + "description": "Disable connection filter safe list.", + "severity": "medium", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.hosted_connection_filter", + "policy_file": "2.1.13_Connection_Filter_SafeList_Off.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.14", + "title": "Ensure inbound anti-spam policies do not contain allowed domains", + "description": "Do not whitelist domains in anti-spam policies.", + "severity": "medium", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.hosted_content_filter", + "policy_file": "2.1.14_Inbound_AntiSpam_Policies_DoNot_AllowedDomains.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.1.15", + "title": "Ensure outbound anti-spam message limits are in place", + "description": "Configure outbound spam message limits.", + "severity": "medium", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.hosted_outbound_spam_filter", + "policy_file": "2.1.15_Outbound_AntiSpam_MessageLimits_InPlace.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "2.2.1", + "title": "Ensure emergency access account activity is monitored", + "description": "Monitor emergency access account sign-ins and usage.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Organizational policy; requires defining which accounts to monitor" + }, + { + "control_id": "2.4.1", + "title": "Ensure Priority account protection is enabled and configured", + "description": "Enable and configure priority account protection.", + "severity": "medium", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Need Priority Account Protection collector" + }, + { + "control_id": "2.4.2", + "title": "Ensure Priority accounts have 'Strict protection' presets applied", + "description": "Apply strict protection presets to priority accounts.", + "severity": "medium", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Need Priority Account Protection collector" + }, + { + "control_id": "2.4.3", + "title": "Ensure Microsoft Defender for Cloud Apps is enabled and configured", + "description": "Enable and configure Microsoft Defender for Cloud Apps.", + "severity": "medium", + "service": "Defender", + "level": "L2", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "MCAS configuration requires portal verification" + }, + { + "control_id": "2.4.4", + "title": "Ensure Zero-hour auto purge for Microsoft Teams is on", + "description": "Enable ZAP for Teams to remove malicious content.", + "severity": "medium", + "service": "Defender", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.protection.teams_protection_policy", + "policy_file": "2.4.4_Zero_hour_AutoPurge_MSTeams_is_On.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "3.1.1", + "title": "Ensure Microsoft 365 audit log search is Enabled", + "description": "Enable unified audit log search.", + "severity": "high", + "service": "Compliance", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "exchange.organization.organization_config", + "policy_file": null, + "requires_permissions": ["Exchange.Manage"], + "notes": "Check AuditDisabled = False" + }, + { + "control_id": "3.2.1", + "title": "Ensure DLP policies are enabled", + "description": "Enable Data Loss Prevention policies.", + "severity": "high", + "service": "Compliance", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "IPPSSession requires certificate auth" + }, + { + "control_id": "3.2.2", + "title": "Ensure DLP policies are enabled for Microsoft Teams", + "description": "Enable DLP policies for Teams.", + "severity": "high", + "service": "Compliance", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "IPPSSession requires certificate auth" + }, + { + "control_id": "3.3.1", + "title": "Ensure Information Protection sensitivity label policies are published", + "description": "Publish sensitivity label policies.", + "severity": "high", + "service": "Compliance", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "IPPSSession requires certificate auth" + }, + { + "control_id": "4.1", + "title": "Ensure devices without a compliance policy are marked 'not compliant'", + "description": "Mark devices without compliance policy as non-compliant.", + "severity": "medium", + "service": "Intune", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.devices.device_management_settings", + "policy_file": "4.1_mark_unmanaged_devices_not_compliant.rego", + "requires_permissions": ["DeviceManagementConfiguration.Read.All"], + "notes": "Collector exists but control logic not defined" + }, + { + "control_id": "4.2", + "title": "Ensure device enrollment for personally owned devices is blocked by default", + "description": "Block personal device enrollment by default.", + "severity": "medium", + "service": "Intune", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.devices.enrollment_restrictions", + "policy_file": "4.2_block_personal_device_enrollment.rego", + "requires_permissions": ["DeviceManagementConfiguration.Read.All"], + "notes": null + }, + { + "control_id": "5.1.2.1", + "title": "Ensure 'Per-user MFA' is disabled", + "description": "Disable per-user MFA in favor of Conditional Access.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Automated", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Only available in beta API; not stable" + }, + { + "control_id": "5.1.2.2", + "title": "Ensure third party integrated applications are not allowed", + "description": "Block third-party application registration.", + "severity": "medium", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.policies.authorization_policy", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Check allowedToCreateApps" + }, + { + "control_id": "5.1.2.3", + "title": "Ensure 'Restrict non-admin users from creating tenants' is set to 'Yes'", + "description": "Prevent non-admin users from creating tenants.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.policies.authorization_policy", + "policy_file": "5.1.2.3_restrict_tenant_creation.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": "Check allowedToCreateTenants" + }, + { + "control_id": "5.1.2.4", + "title": "Ensure access to the Entra admin center is restricted", + "description": "Restrict access to Entra admin center.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Only available via internal Azure API" + }, + { + "control_id": "5.1.2.5", + "title": "Ensure the option to remain signed in is hidden", + "description": "Hide the 'Stay signed in' option.", + "severity": "low", + "service": "EntraID", + "level": "L2", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Setting not exposed via Graph API" + }, + { + "control_id": "5.1.2.6", + "title": "Ensure 'LinkedIn account connections' is disabled", + "description": "Disable LinkedIn account connections.", + "severity": "low", + "service": "EntraID", + "level": "L2", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Only available via internal Azure API" + }, + { + "control_id": "5.1.3.1", + "title": "Ensure a dynamic group for guest users is created", + "description": "Create a dynamic group to manage guest users.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.groups.groups", + "policy_file": "5.1.3.1_dynamic_guest_group_exists.rego", + "requires_permissions": ["Group.Read.All"], + "notes": "Collector exists but control logic not defined" + }, + { + "control_id": "5.1.3.2", + "title": "Ensure users cannot create security groups", + "description": "Prevent users from creating security groups.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.policies.authorization_policy", + "policy_file": "5.1.3.2_block_security_group_creation.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": "Check allowedToCreateSecurityGroups" + }, + { + "control_id": "5.1.4.1", + "title": "Ensure the ability to join devices to Entra is restricted", + "description": "Restrict device join to authorized users.", + "severity": "medium", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.devices.device_registration_policy", + "policy_file": "5.1.4.1_restrict_device_join.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.1.4.2", + "title": "Ensure the maximum number of devices per user is limited", + "description": "Limit devices a user can register.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "entra.devices.device_management_settings", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.1.4.3", + "title": "Ensure the GA role is not added as a local administrator during Entra join", + "description": "Do not add Global Admin as local admin on joined devices.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "entra.devices.device_management_settings", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.1.4.4", + "title": "Ensure local administrator assignment is limited during Entra join", + "description": "Limit local administrator assignment on joined devices.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "entra.devices.device_management_settings", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.1.4.5", + "title": "Ensure Local Administrator Password Solution is enabled", + "description": "Enable LAPS for local admin password management.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Need LAPS configuration collector" + }, + { + "control_id": "5.1.4.6", + "title": "Ensure users are restricted from recovering BitLocker keys", + "description": "Restrict BitLocker key recovery to admins.", + "severity": "medium", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.policies.authorization_policy", + "policy_file": "5.1.4.6_restrict_bitlocker_key_recovery.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": "Check allowedToReadBitlockerKeysForOwnedDevice" + }, + { + "control_id": "5.1.5.1", + "title": "Ensure user consent to apps accessing company data on their behalf is not allowed", + "description": "Block user consent to apps.", + "severity": "high", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.policies.authorization_policy", + "policy_file": "5.1.5.1_block_user_app_consent.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.1.5.2", + "title": "Ensure the admin consent workflow is enabled", + "description": "Enable admin consent workflow for apps.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.policies.admin_consent_request_policy", + "policy_file": "5.1.5.2_admin_consent_workflow_enabled.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.1.6.1", + "title": "Ensure that collaboration invitations are sent to allowed domains only", + "description": "Restrict B2B invitations to allowed domains.", + "severity": "medium", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.policies.b2b_policy", + "policy_file": "5.1.6.1_restrict_collaboration_invite_domains.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.1.6.2", + "title": "Ensure that guest user access is restricted", + "description": "Restrict guest user access permissions.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.policies.authorization_policy", + "policy_file": "5.1.6.2_restrict_guest_user_access.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": "Check guestUserRoleId" + }, + { + "control_id": "5.1.6.3", + "title": "Ensure guest user invitations are limited to the Guest Inviter role", + "description": "Limit who can invite guest users.", + "severity": "medium", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.policies.authorization_policy", + "policy_file": "5.1.6.3_limit_guest_invitations.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": "Check allowInvitesFrom" + }, + { + "control_id": "5.1.8.1", + "title": "Ensure that password hash sync is enabled for hybrid deployments", + "description": "Enable password hash sync for hybrid identity.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Requires on-premises AD Connect verification" + }, + { + "control_id": "5.2.2.1", + "title": "Ensure multifactor authentication is enabled for all users in administrative roles", + "description": "Require MFA for all administrative roles.", + "severity": "critical", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires verification of all 15 admin roles" + }, + { + "control_id": "5.2.2.2", + "title": "Ensure multifactor authentication is enabled for all users", + "description": "Require MFA for all users.", + "severity": "critical", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires verification of exclusions" + }, + { + "control_id": "5.2.2.3", + "title": "Enable Conditional Access policies to block legacy authentication", + "description": "Block legacy authentication protocols.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.conditional_access.legacy_auth_block", + "policy_file": "5.2.2.3_block_legacy_auth.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.2.2.4", + "title": "Ensure Sign-in frequency is enabled and browser sessions are not persistent for Administrative users", + "description": "Configure sign-in frequency for administrators.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires admin role verification" + }, + { + "control_id": "5.2.2.5", + "title": "Ensure 'Phishing-resistant MFA strength' is required for Administrators", + "description": "Require phishing-resistant MFA for administrators.", + "severity": "high", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires admin role verification" + }, + { + "control_id": "5.2.2.6", + "title": "Enable Identity Protection user risk policies", + "description": "Enable user risk policies in Identity Protection.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires policy coverage verification" + }, + { + "control_id": "5.2.2.7", + "title": "Enable Identity Protection sign-in risk policies", + "description": "Enable sign-in risk policies in Identity Protection.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires policy coverage verification" + }, + { + "control_id": "5.2.2.8", + "title": "Ensure 'sign-in risk' is blocked for medium and high risk", + "description": "Block sign-ins with medium or high risk.", + "severity": "high", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires policy coverage verification" + }, + { + "control_id": "5.2.2.9", + "title": "Ensure a managed device is required for authentication", + "description": "Require managed devices for authentication.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires policy coverage verification" + }, + { + "control_id": "5.2.2.10", + "title": "Ensure a managed device is required to register security information", + "description": "Require managed device for security info registration.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires policy coverage verification" + }, + { + "control_id": "5.2.2.11", + "title": "Ensure sign-in frequency for Intune Enrollment is set to 'Every time'", + "description": "Require authentication for every Intune enrollment.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires policy coverage verification" + }, + { + "control_id": "5.2.2.12", + "title": "Ensure the device code sign-in flow is blocked", + "description": "Block device code authentication flow.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "deferred", + "data_collector_id": "entra.conditional_access.policies", + "policy_file": null, + "requires_permissions": ["Policy.Read.All"], + "notes": "Requires policy coverage verification" + }, + { + "control_id": "5.2.3.1", + "title": "Ensure Microsoft Authenticator is configured to protect against MFA fatigue", + "description": "Configure Authenticator to prevent MFA fatigue attacks.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.authentication.mfa_fatigue_protection", + "policy_file": "5.2.3.1_mfa_fatigue_protection.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.2.3.2", + "title": "Ensure custom banned passwords lists are used", + "description": "Configure custom banned password list.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.authentication.password_protection", + "policy_file": "5.2.3.2_custom_banned_passwords_enabled.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.2.3.3", + "title": "Ensure password protection is enabled for on-prem Active Directory", + "description": "Enable password protection for on-premises AD.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.authentication.password_protection", + "policy_file": "5.2.3.3_enable_on_prem_password_protection.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.2.3.4", + "title": "Ensure all member users are 'MFA capable'", + "description": "Ensure all users have registered MFA methods.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.authentication.mfa_registration_report", + "policy_file": "5.2.3.4_all_members_mfa_capable.rego", + "requires_permissions": ["AuditLog.Read.All"], + "notes": null + }, + { + "control_id": "5.2.3.5", + "title": "Ensure weak authentication methods are disabled", + "description": "Disable SMS and voice authentication methods.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.authentication.authentication_methods", + "policy_file": "5.2.3.5_disable_weak_auth_methods.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": "Check SMS/Voice disabled" + }, + { + "control_id": "5.2.3.6", + "title": "Ensure system-preferred multifactor authentication is enabled", + "description": "Enable system-preferred MFA.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.authentication.authentication_methods", + "policy_file": "5.2.3.6_enable_system_preferred_mfa.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.2.3.7", + "title": "Ensure the email OTP authentication method is disabled", + "description": "Disable email OTP authentication.", + "severity": "medium", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.authentication.authentication_methods", + "policy_file": "5.2.3.7_disable_email_otp.rego", + "requires_permissions": ["Policy.Read.All"], + "notes": null + }, + { + "control_id": "5.2.4.1", + "title": "Ensure 'Self service password reset enabled' is set to 'All'", + "description": "Enable SSPR for all users.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "SSPR settings not exposed via Graph API" + }, + { + "control_id": "5.3.1", + "title": "Ensure 'Privileged Identity Management' is used to manage roles", + "description": "Use PIM for privileged role management.", + "severity": "high", + "service": "EntraID", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.governance.pim_role_policies", + "policy_file": "5.3.1_pim_enabled.rego", + "requires_permissions": ["RoleManagement.Read.Directory"], + "notes": null + }, + { + "control_id": "5.3.2", + "title": "Ensure 'Access reviews' for Guest Users are configured", + "description": "Configure access reviews for guest users.", + "severity": "medium", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.governance.access_reviews", + "policy_file": "5.3.2_guest_access_reviews_configured.rego", + "requires_permissions": ["AccessReview.Read.All"], + "notes": null + }, + { + "control_id": "5.3.3", + "title": "Ensure 'Access reviews' for privileged roles are configured", + "description": "Configure access reviews for privileged roles.", + "severity": "high", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.governance.access_reviews", + "policy_file": "5.3.3_privileged_role_access_reviews_configured.rego", + "requires_permissions": ["AccessReview.Read.All"], + "notes": "Combined with pim_role_policies" + }, + { + "control_id": "5.3.4", + "title": "Ensure approval is required for Global Administrator role activation", + "description": "Require approval for Global Admin role activation.", + "severity": "critical", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.governance.pim_role_policies", + "policy_file": "5.3.4_ga_activation_requires_approval.rego", + "requires_permissions": ["RoleManagement.Read.Directory"], + "notes": null + }, + { + "control_id": "5.3.5", + "title": "Ensure approval is required for Privileged Role Administrator activation", + "description": "Require approval for Privileged Role Admin activation.", + "severity": "critical", + "service": "EntraID", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "entra.governance.pim_role_policies", + "policy_file": "5.3.5_pra_activation_requires_approval.rego", + "requires_permissions": ["RoleManagement.Read.Directory"], + "notes": null + }, + { + "control_id": "6.1.1", + "title": "Ensure 'AuditDisabled' organizationally is set to 'False'", + "description": "Ensure organization-wide auditing is enabled.", + "severity": "high", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.organization.organization_config", + "policy_file": "6.1.1_audit_disabled.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.1.2", + "title": "Ensure mailbox audit actions are configured", + "description": "Configure comprehensive mailbox audit actions.", + "severity": "medium", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.mailbox.mailbox_audit_actions", + "policy_file": "6.1.2_mailbox_audit_actions.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.1.3", + "title": "Ensure 'AuditBypassEnabled' is not enabled on mailboxes", + "description": "Ensure no mailboxes bypass auditing.", + "severity": "medium", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.mailbox.mailbox_audit", + "policy_file": "6.1.3_audit_bypass.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.2.1", + "title": "Ensure all forms of mail forwarding are blocked and/or disabled", + "description": "Block all forms of mail forwarding.", + "severity": "high", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.transport.transport_rules", + "policy_file": "6.2.1_mail_forwarding_blocked.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.2.2", + "title": "Ensure mail transport rules do not whitelist specific domains", + "description": "Do not whitelist domains in transport rules.", + "severity": "medium", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.transport.transport_rules", + "policy_file": "6.2.2_transport_whitelist.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.2.3", + "title": "Ensure email from external senders is identified", + "description": "Identify emails from external senders.", + "severity": "medium", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.transport.external_in_outlook", + "policy_file": "6.2.3_external_sender_tagging.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.3.1", + "title": "Ensure users installing Outlook add-ins is not allowed", + "description": "Block user installation of Outlook add-ins.", + "severity": "medium", + "service": "Exchange", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.mailbox.role_assignment_policy", + "policy_file": "6.3.1_outlook_addins.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.5.1", + "title": "Ensure modern authentication for Exchange Online is enabled", + "description": "Enable modern authentication for Exchange.", + "severity": "high", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.organization.organization_config", + "policy_file": "6.5.1_modern_auth.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": "Check OAuth2ClientProfileEnabled" + }, + { + "control_id": "6.5.2", + "title": "Ensure MailTips are enabled for end users", + "description": "Enable MailTips for users.", + "severity": "low", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.organization.organization_config", + "policy_file": "6.5.2_mailtips.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.5.3", + "title": "Ensure additional storage providers are restricted in Outlook on the web", + "description": "Restrict storage providers in OWA.", + "severity": "medium", + "service": "Exchange", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.organization.owa_mailbox_policy", + "policy_file": "6.5.3_storage_providers.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.5.4", + "title": "Ensure SMTP AUTH is disabled", + "description": "Disable SMTP AUTH for the organization.", + "severity": "medium", + "service": "Exchange", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.organization.transport_config", + "policy_file": "6.5.4_smtp_auth.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "6.5.5", + "title": "Ensure Direct Send submissions are rejected", + "description": "Reject Direct Send submissions.", + "severity": "medium", + "service": "Exchange", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "ready", + "data_collector_id": "exchange.organization.organization_config", + "policy_file": "6.5.5_direct_send.rego", + "requires_permissions": ["Exchange.Manage"], + "notes": null + }, + { + "control_id": "7.2.1", + "title": "Ensure modern authentication for SharePoint applications is required", + "description": "Require modern auth for SharePoint.", + "severity": "high", + "service": "SharePoint", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.2.2", + "title": "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled", + "description": "Enable B2B integration for SharePoint.", + "severity": "medium", + "service": "SharePoint", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.2.3", + "title": "Ensure external content sharing is restricted", + "description": "Restrict external sharing of content.", + "severity": "medium", + "service": "SharePoint", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.2.4", + "title": "Ensure OneDrive content sharing is restricted", + "description": "Restrict OneDrive content sharing.", + "severity": "medium", + "service": "SharePoint", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.2.5", + "title": "Ensure that SharePoint guest users cannot share items they don't own", + "description": "Restrict guest resharing permissions.", + "severity": "medium", + "service": "SharePoint", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.2.6", + "title": "Ensure SharePoint external sharing is restricted", + "description": "Restrict SharePoint external sharing.", + "severity": "medium", + "service": "SharePoint", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.2.7", + "title": "Ensure link sharing is restricted in SharePoint and OneDrive", + "description": "Restrict link sharing settings.", + "severity": "medium", + "service": "SharePoint", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.2.8", + "title": "Ensure external sharing is restricted by security group", + "description": "Restrict external sharing to security group members.", + "severity": "medium", + "service": "SharePoint", + "level": "L2", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError; CIS says Manual but can be automated" + }, + { + "control_id": "7.2.9", + "title": "Ensure guest access to a site or OneDrive will expire automatically", + "description": "Configure automatic guest access expiration.", + "severity": "medium", + "service": "SharePoint", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.2.10", + "title": "Ensure reauthentication with verification code is restricted", + "description": "Restrict verification code reauthentication.", + "severity": "medium", + "service": "SharePoint", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.2.11", + "title": "Ensure the SharePoint default sharing link permission is set", + "description": "Configure default sharing link permissions.", + "severity": "medium", + "service": "SharePoint", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.3.1", + "title": "Ensure Office 365 SharePoint infected files are disallowed for download", + "description": "Block download of infected files.", + "severity": "high", + "service": "SharePoint", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_tenant", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector raises NotImplementedError" + }, + { + "control_id": "7.3.2", + "title": "Ensure OneDrive sync is restricted for unmanaged devices", + "description": "Restrict OneDrive sync to managed devices.", + "severity": "medium", + "service": "SharePoint", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "not_started", + "data_collector_id": "sharepoint.spo_sync_client_restriction", + "policy_file": null, + "requires_permissions": ["SharePoint.Admin"], + "notes": "Collector exists but control logic not defined" + }, + { + "control_id": "8.1.1", + "title": "Ensure external file sharing in Teams is enabled for only approved cloud storage services", + "description": "Restrict Teams file sharing to approved services.", + "severity": "medium", + "service": "Teams", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.1.2", + "title": "Ensure users can't send emails to a channel email address", + "description": "Disable sending emails to channel addresses.", + "severity": "low", + "service": "Teams", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.2.1", + "title": "Ensure external domains are restricted in the Teams admin center", + "description": "Restrict external domain communication.", + "severity": "medium", + "service": "Teams", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.2.2", + "title": "Ensure communication with unmanaged Teams users is disabled", + "description": "Block communication with unmanaged Teams.", + "severity": "medium", + "service": "Teams", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.2.3", + "title": "Ensure external Teams users cannot initiate conversations", + "description": "Prevent external users from starting conversations.", + "severity": "medium", + "service": "Teams", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.2.4", + "title": "Ensure the organization cannot communicate with accounts in trial Teams tenants", + "description": "Block communication with trial tenant accounts.", + "severity": "medium", + "service": "Teams", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.4.1", + "title": "Ensure app permission policies are configured", + "description": "Configure Teams app permission policies.", + "severity": "medium", + "service": "Teams", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "manual", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "CIS marks as Manual; also affected by ACM migration" + }, + { + "control_id": "8.5.1", + "title": "Ensure anonymous users can't join a meeting", + "description": "Block anonymous users from joining meetings.", + "severity": "medium", + "service": "Teams", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.5.2", + "title": "Ensure anonymous users and dial-in callers can't start a meeting", + "description": "Prevent anonymous users from starting meetings.", + "severity": "medium", + "service": "Teams", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.5.3", + "title": "Ensure only people in my org can bypass the lobby", + "description": "Restrict lobby bypass to organization members.", + "severity": "medium", + "service": "Teams", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.5.4", + "title": "Ensure users dialing in can't bypass the lobby", + "description": "Dial-in users must wait in lobby.", + "severity": "medium", + "service": "Teams", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.5.5", + "title": "Ensure meeting chat does not allow anonymous users", + "description": "Block anonymous users from meeting chat.", + "severity": "medium", + "service": "Teams", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.5.6", + "title": "Ensure only organizers and co-organizers can present", + "description": "Restrict presenting to organizers.", + "severity": "low", + "service": "Teams", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.5.7", + "title": "Ensure external participants can't give or request control", + "description": "Block external control in meetings.", + "severity": "medium", + "service": "Teams", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.5.8", + "title": "Ensure external meeting chat is off", + "description": "Disable external meeting chat.", + "severity": "medium", + "service": "Teams", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.5.9", + "title": "Ensure meeting recording is off by default", + "description": "Disable default meeting recording.", + "severity": "medium", + "service": "Teams", + "level": "L2", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "8.6.1", + "title": "Ensure users can report security concerns in Teams", + "description": "Enable security concern reporting in Teams.", + "severity": "medium", + "service": "Teams", + "level": "L1", + "is_manual": false, + "benchmark_audit_type": "Automated", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Teams module AccessTokens auth not working" + }, + { + "control_id": "9.1.1", + "title": "Ensure guest user access is restricted", + "description": "Restrict guest access in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.2", + "title": "Ensure external user invitations are restricted", + "description": "Restrict external invitations in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.3", + "title": "Ensure guest access to content is restricted", + "description": "Restrict guest content access in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.4", + "title": "Ensure 'Publish to web' is restricted", + "description": "Restrict publish to web in Fabric.", + "severity": "high", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.5", + "title": "Ensure 'Interact with and share R and Python' visuals is 'Disabled'", + "description": "Disable R and Python visual sharing.", + "severity": "medium", + "service": "Fabric", + "level": "L2", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.6", + "title": "Ensure 'Allow users to apply sensitivity labels for content' is 'Enabled'", + "description": "Enable sensitivity labels in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.7", + "title": "Ensure shareable links are restricted", + "description": "Restrict shareable links in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.8", + "title": "Ensure enabling of external data sharing is restricted", + "description": "Restrict external data sharing in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.9", + "title": "Ensure 'Block ResourceKey Authentication' is 'Enabled'", + "description": "Block ResourceKey authentication in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.10", + "title": "Ensure access to APIs by service principals is restricted", + "description": "Restrict service principal API access in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.11", + "title": "Ensure service principals cannot create and use profiles", + "description": "Restrict service principal profiles in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + }, + { + "control_id": "9.1.12", + "title": "Ensure service principals ability to create workspaces, connections and deployment pipelines is restricted", + "description": "Restrict service principal workspace creation in Fabric.", + "severity": "medium", + "service": "Fabric", + "level": "L1", + "is_manual": true, + "benchmark_audit_type": "Manual", + "automation_status": "blocked", + "data_collector_id": null, + "policy_file": null, + "requires_permissions": null, + "notes": "Fabric API auth untested; CIS says Manual but can be automated" + } + ] +} diff --git a/engine/powershell/Dockerfile b/engine/powershell/Dockerfile new file mode 100644 index 00000000..a7e06373 --- /dev/null +++ b/engine/powershell/Dockerfile @@ -0,0 +1,26 @@ +FROM mcr.microsoft.com/powershell:7.5-mariner-2.0 + +# Install Python, curl (for healthcheck), and tar (for uv installer) +RUN tdnf install -y python3 curl tar && tdnf clean all + +# Install uv +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:$PATH" + +# Install PowerShell modules +RUN pwsh -NoProfile -Command " \ + Set-PSRepository PSGallery -InstallationPolicy Trusted; \ + Install-Module -Name ExchangeOnlineManagement -Scope AllUsers -Force; \ + Install-Module -Name MicrosoftTeams -Scope AllUsers -Force \ +" + +# Copy service code and install Python dependencies +COPY service/ /app/ +WORKDIR /app +RUN uv pip install --system -r pyproject.toml + +# Expose port +EXPOSE 8001 + +# Run FastAPI service +CMD ["python3", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/engine/powershell/service/executor.py b/engine/powershell/service/executor.py new file mode 100644 index 00000000..de0881cd --- /dev/null +++ b/engine/powershell/service/executor.py @@ -0,0 +1,171 @@ +"""PowerShell command executor.""" + +import json +import os +import subprocess +from typing import Any, Dict, Optional + + +class PowerShellExecutionError(Exception): + """Raised when PowerShell execution fails.""" + + pass + + +def build_param_string(params: Dict[str, Any]) -> str: + """Build PowerShell parameter string from dict. + + Args: + params: Dictionary of parameter names to values + + Returns: + PowerShell parameter string (e.g., ' -Name "value" -Enabled:$true') + """ + param_str = "" + for key, value in params.items(): + if isinstance(value, bool): + param_str += f" -{key}:${str(value).lower()}" + elif isinstance(value, str): + # Escape double quotes in string values + escaped = value.replace('"', '`"') + param_str += f' -{key} "{escaped}"' + else: + param_str += f" -{key} {value}" + return param_str + + +def build_script( + module: str, + cmdlet: str, + params: Dict[str, Any], + tenant_id: str, +) -> str: + """Build the PowerShell script to execute. + + Args: + module: The module to use (ExchangeOnline, Compliance, Teams) + cmdlet: The cmdlet to run + params: Parameters for the cmdlet + tenant_id: Azure AD tenant ID + + Returns: + PowerShell script as a string + """ + param_str = build_param_string(params) + + if module == "ExchangeOnline": + return f''' +Import-Module ExchangeOnlineManagement +Connect-ExchangeOnline -AccessToken $env:EXO_TOKEN -Organization "{tenant_id}" -ShowBanner:$false +try {{ + $result = {cmdlet}{param_str} + if ($null -eq $result) {{ + Write-Output 'null' + }} else {{ + $result | ConvertTo-Json -Depth 10 + }} +}} finally {{ + Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue +}} +''' + elif module == "Compliance": + return f''' +Import-Module ExchangeOnlineManagement +Connect-IPPSSession -AccessToken $env:EXO_TOKEN -Organization "{tenant_id}" -ShowBanner:$false +try {{ + $result = {cmdlet}{param_str} + if ($null -eq $result) {{ + Write-Output 'null' + }} else {{ + $result | ConvertTo-Json -Depth 10 + }} +}} finally {{ + Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue +}} +''' + elif module == "Teams": + return f''' +Import-Module MicrosoftTeams +Connect-MicrosoftTeams -AccessTokens @($env:GRAPH_TOKEN, $env:TEAMS_TOKEN) -TenantId "{tenant_id}" +try {{ + $result = {cmdlet}{param_str} + if ($null -eq $result) {{ + Write-Output 'null' + }} else {{ + $result | ConvertTo-Json -Depth 10 + }} +}} finally {{ + Disconnect-MicrosoftTeams -ErrorAction SilentlyContinue +}} +''' + else: + raise ValueError(f"Unsupported module: {module}") + + +def execute_cmdlet( + module: str, + cmdlet: str, + params: Dict[str, Any], + tenant_id: str, + token: str, + graph_token: Optional[str] = None, +) -> Dict[str, Any]: + """Execute a PowerShell cmdlet and return the result. + + Args: + module: PowerShell module (ExchangeOnline, Compliance, Teams) + cmdlet: The cmdlet to run + params: Parameters for the cmdlet + tenant_id: Azure AD tenant ID + token: Access token for Exchange/Compliance + graph_token: Graph API token (required for Teams) + + Returns: + Parsed JSON output from the cmdlet + + Raises: + PowerShellExecutionError: If execution fails + ValueError: If Teams module requested without graph_token + """ + if module == "Teams" and not graph_token: + raise ValueError("Teams module requires graph_token") + + # Build the script + script = build_script(module, cmdlet, params, tenant_id) + + # Set up environment with tokens + env = os.environ.copy() + if module == "Teams": + env["GRAPH_TOKEN"] = graph_token + env["TEAMS_TOKEN"] = token + else: + env["EXO_TOKEN"] = token + + # Execute PowerShell + try: + proc = subprocess.run( + ["pwsh", "-NoProfile", "-NonInteractive", "-Command", script], + capture_output=True, + text=True, + timeout=120, + env=env, + ) + except subprocess.TimeoutExpired: + raise PowerShellExecutionError("PowerShell execution timed out after 120 seconds") + except Exception as e: + raise PowerShellExecutionError(f"Failed to execute PowerShell: {e}") + + if proc.returncode != 0: + raise PowerShellExecutionError(f"PowerShell execution failed:\n{proc.stderr}") + + # Parse JSON output + stdout = proc.stdout.strip() + if not stdout or stdout == "null": + return None + + try: + return json.loads(stdout) + except json.JSONDecodeError as e: + raise PowerShellExecutionError( + f"Failed to parse PowerShell output as JSON:\n{stdout}\nError: {e}" + ) diff --git a/engine/powershell/service/main.py b/engine/powershell/service/main.py new file mode 100644 index 00000000..95f84988 --- /dev/null +++ b/engine/powershell/service/main.py @@ -0,0 +1,49 @@ +"""FastAPI service for PowerShell cmdlet execution.""" + +from fastapi import FastAPI, HTTPException + +from schemas import ExecuteRequest, ExecuteResponse, HealthResponse +from executor import execute_cmdlet, PowerShellExecutionError + +app = FastAPI( + title="PowerShell Service", + description="HTTP service for executing M365 PowerShell cmdlets", + version="1.0.0", +) + + +@app.get("/health", response_model=HealthResponse) +async def health_check(): + """Health check endpoint.""" + return HealthResponse(status="ok") + + +@app.post("/execute", response_model=ExecuteResponse) +async def execute(request: ExecuteRequest): + """Execute a PowerShell cmdlet. + + Args: + request: Execution request with module, cmdlet, params, and auth + + Returns: + ExecuteResponse with success status and data or error + """ + try: + result = execute_cmdlet( + module=request.module, + cmdlet=request.cmdlet, + params=request.params, + tenant_id=request.tenant_id, + token=request.token, + graph_token=request.graph_token, + ) + return ExecuteResponse(success=True, data=result) + except ValueError as e: + # Invalid request (e.g., Teams without graph_token) + raise HTTPException(status_code=400, detail=str(e)) + except PowerShellExecutionError as e: + # PowerShell execution failed + return ExecuteResponse(success=False, error=str(e)) + except Exception as e: + # Unexpected error + return ExecuteResponse(success=False, error=f"Unexpected error: {e}") diff --git a/engine/powershell/service/pyproject.toml b/engine/powershell/service/pyproject.toml new file mode 100644 index 00000000..56067bd7 --- /dev/null +++ b/engine/powershell/service/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "powershell-service" +version = "1.0.0" +description = "HTTP service for executing M365 PowerShell cmdlets" +requires-python = ">=3.9" +dependencies = [ + "fastapi", + "uvicorn", + "pydantic", +] diff --git a/engine/powershell/service/schemas.py b/engine/powershell/service/schemas.py new file mode 100644 index 00000000..abbed3a2 --- /dev/null +++ b/engine/powershell/service/schemas.py @@ -0,0 +1,40 @@ +"""Pydantic schemas for PowerShell service API.""" + +from typing import Any, Dict, Literal, Optional + +from pydantic import BaseModel, Field + + +class ExecuteRequest(BaseModel): + """Request to execute a PowerShell cmdlet.""" + + module: Literal["ExchangeOnline", "Compliance", "Teams"] = Field( + description="PowerShell module to use" + ) + cmdlet: str = Field( + description="PowerShell cmdlet to execute (e.g., Get-OrganizationConfig)" + ) + params: Dict[str, Any] = Field( + default_factory=dict, + description="Parameters to pass to the cmdlet", + ) + tenant_id: str = Field(description="Azure AD tenant ID") + token: str = Field(description="Access token for Exchange/Compliance") + graph_token: Optional[str] = Field( + default=None, + description="Graph API token (required for Teams module)", + ) + + +class ExecuteResponse(BaseModel): + """Response from PowerShell cmdlet execution.""" + + success: bool = Field(description="Whether execution succeeded") + data: Any = Field(default=None, description="Cmdlet output as JSON") + error: Optional[str] = Field(default=None, description="Error message if failed") + + +class HealthResponse(BaseModel): + """Health check response.""" + + status: str = "ok" diff --git a/engine/pyproject.toml b/engine/pyproject.toml new file mode 100644 index 00000000..35fdaf7f --- /dev/null +++ b/engine/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "autoaudit-worker" +version = "0.1.0" +description = "AutoAudit Celery worker for compliance scanning" +requires-python = ">=3.10" +dependencies = [ + "celery[redis]>=5.3.0", + "httpx>=0.26.0", + "msal>=1.31.0", + "sqlalchemy>=2.0.0", + "psycopg2-binary>=2.9.9", + "pydantic>=2.5.0", + "pydantic-settings>=2.1.0", + "cryptography>=42.0.0", + "dnspython>=2.4.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["worker", "collectors"] +include = ["opa_client.py"] diff --git a/engine/scripts/__init__.py b/engine/scripts/__init__.py new file mode 100644 index 00000000..b0cd298c --- /dev/null +++ b/engine/scripts/__init__.py @@ -0,0 +1 @@ +# Scripts package for standalone utilities. diff --git a/engine/scripts/test_collector.py b/engine/scripts/test_collector.py new file mode 100644 index 00000000..50639316 --- /dev/null +++ b/engine/scripts/test_collector.py @@ -0,0 +1,324 @@ +"""Standalone collector test script for live M365 tenant testing. + +This script bypasses the worker/OPA flow to test collectors directly +and document actual API response payloads. + +Usage: + cd engine + python -m scripts.test_collector --list + python -m scripts.test_collector -c entra.roles.cloud_only_admins + python -m scripts.test_collector -c entra.conditional_access.policies -o ./samples/ + +Environment Variables: + M365_TENANT_ID: Azure AD tenant ID + M365_CLIENT_ID: App registration client ID + M365_CLIENT_SECRET: App registration client secret +""" + +import argparse +import asyncio +import json +import os +import sys +from datetime import datetime +from pathlib import Path + +# Add parent to path for imports when running as module +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from collectors.registry import DATA_COLLECTORS, get_collector +from collectors.graph_client import GraphClient +from collectors.powershell_base import BasePowerShellCollector +from collectors.powershell_client import PowerShellClient, PowerShellExecutionError + + +def list_collectors() -> None: + """Print all registered collectors.""" + print("Available collectors:") + print("-" * 50) + for collector_id in sorted(DATA_COLLECTORS.keys()): + collector_class = DATA_COLLECTORS[collector_id] + doc = collector_class.__doc__ or "No description" + # Get first line of docstring + doc_line = doc.strip().split("\n")[0] + print(f" {collector_id}") + print(f" {doc_line}") + print() + + +def get_credentials() -> tuple[str, str, str]: + """Get M365 credentials from environment variables.""" + tenant_id = os.environ.get("M365_TENANT_ID") + client_id = os.environ.get("M365_CLIENT_ID") + client_secret = os.environ.get("M365_CLIENT_SECRET") + + missing = [] + if not tenant_id: + missing.append("M365_TENANT_ID") + if not client_id: + missing.append("M365_CLIENT_ID") + if not client_secret: + missing.append("M365_CLIENT_SECRET") + + if missing: + print("Error: Missing required environment variables:") + for var in missing: + print(f" - {var}") + print("\nSet these variables before running the script:") + print(" export M365_TENANT_ID=") + print(" export M365_CLIENT_ID=") + print(" export M365_CLIENT_SECRET=") + sys.exit(1) + + return tenant_id, client_id, client_secret + + +async def test_collector( + collector_id: str, + output_dir: Path | None = None, + verbose: bool = False, + service_url: str | None = None, +) -> dict: + """Run a collector and return/save results. + + Args: + collector_id: The collector ID to run (e.g., 'entra.roles.cloud_only_admins') + output_dir: Optional directory to save JSON output + verbose: If True, print additional debug info + service_url: Optional PowerShell HTTP service URL + + Returns: + Dict containing collector_id, timestamp, elapsed_seconds, and data + """ + # Validate collector exists + if collector_id not in DATA_COLLECTORS: + print(f"Error: Unknown collector '{collector_id}'") + print("\nAvailable collectors:") + for cid in sorted(DATA_COLLECTORS.keys()): + print(f" {cid}") + sys.exit(1) + + # Get credentials + tenant_id, client_id, client_secret = get_credentials() + + if verbose: + print(f"Tenant ID: {tenant_id}") + print(f"Client ID: {client_id}") + if service_url: + print(f"Using PowerShell service: {service_url}") + print() + + # Create collector and appropriate client + collector = get_collector(collector_id) + if isinstance(collector, BasePowerShellCollector): + client = PowerShellClient(tenant_id, client_id, client_secret, service_url=service_url) + else: + client = GraphClient(tenant_id, client_id, client_secret) + + # Run collection + print(f"Running collector: {collector_id}") + print(f"Collector class: {collector.__class__.__name__}") + print("-" * 50) + + start = datetime.now() + try: + result = await collector.collect(client) + elapsed = (datetime.now() - start).total_seconds() + print(f"Collection completed in {elapsed:.2f}s") + except NotImplementedError: + print("Error: This collector is not yet implemented (stub only)") + sys.exit(1) + except PowerShellExecutionError as e: + print(f"PowerShell Error: {e}") + sys.exit(1) + except Exception as e: + print(f"Error during collection: {type(e).__name__}: {e}") + if verbose: + import traceback + traceback.print_exc() + sys.exit(1) + + # Build output structure + output = { + "collector_id": collector_id, + "timestamp": datetime.now().isoformat(), + "elapsed_seconds": round(elapsed, 3), + "data": result, + } + + # Pretty print to console + print("-" * 50) + print("Result:") + print(json.dumps(output, indent=2, default=str)) + + # Save to file if output_dir specified + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + filename = f"{collector_id.replace('.', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + filepath = output_dir / filename + with open(filepath, "w") as f: + json.dump(output, f, indent=2, default=str) + print() + print(f"Saved to: {filepath}") + + return output + + +async def test_all_collectors( + output_dir: Path | None = None, + service_url: str | None = None, +) -> None: + """Test all registered collectors and report status.""" + print("Testing all registered collectors...") + if service_url: + print(f"Using PowerShell service: {service_url}") + print("=" * 60) + + tenant_id, client_id, client_secret = get_credentials() + graph_client = GraphClient(tenant_id, client_id, client_secret) + ps_client = None # Lazy init PowerShell client + + results = [] + for collector_id in sorted(DATA_COLLECTORS.keys()): + print(f"\n{collector_id}:") + collector = get_collector(collector_id) + + # Use appropriate client based on collector type + if isinstance(collector, BasePowerShellCollector): + if ps_client is None: + ps_client = PowerShellClient(tenant_id, client_id, client_secret, service_url=service_url) + client = ps_client + else: + client = graph_client + + start = datetime.now() + try: + result = await collector.collect(client) + elapsed = (datetime.now() - start).total_seconds() + status = "OK" + error = None + print(f" Status: OK ({elapsed:.2f}s)") + except NotImplementedError: + status = "NOT_IMPLEMENTED" + error = "Stub only" + elapsed = 0 + print(f" Status: NOT_IMPLEMENTED (stub)") + except PowerShellExecutionError as e: + status = "POWERSHELL_ERROR" + error = str(e) + elapsed = 0 + print(f" Status: POWERSHELL_ERROR - {e}") + except Exception as e: + status = "ERROR" + error = str(e) + elapsed = 0 + print(f" Status: ERROR - {e}") + + results.append({ + "collector_id": collector_id, + "status": status, + "elapsed_seconds": round(elapsed, 3) if elapsed else None, + "error": error, + }) + + # Print summary + print("\n" + "=" * 60) + print("Summary:") + ok_count = sum(1 for r in results if r["status"] == "OK") + stub_count = sum(1 for r in results if r["status"] == "NOT_IMPLEMENTED") + ps_error_count = sum(1 for r in results if r["status"] == "POWERSHELL_ERROR") + error_count = sum(1 for r in results if r["status"] == "ERROR") + print(f" OK: {ok_count}") + print(f" Not Implemented: {stub_count}") + print(f" PowerShell Errors: {ps_error_count}") + print(f" Other Errors: {error_count}") + + # Save summary if output_dir specified + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + filepath = output_dir / f"test_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + with open(filepath, "w") as f: + json.dump({ + "timestamp": datetime.now().isoformat(), + "summary": { + "ok": ok_count, + "not_implemented": stub_count, + "powershell_errors": ps_error_count, + "errors": error_count, + }, + "results": results, + }, f, indent=2) + print(f"\nSummary saved to: {filepath}") + + +def main() -> None: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Test M365 collectors against live tenant", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python -m scripts.test_collector --list + python -m scripts.test_collector -c entra.roles.cloud_only_admins + python -m scripts.test_collector -c entra.roles.cloud_only_admins -o ./samples/ + python -m scripts.test_collector --all -o ./samples/ + + # Use PowerShell HTTP service instead of local Docker + python -m scripts.test_collector -c exchange.organization.organization_config --use-service http://localhost:8001 + +Environment Variables: + M365_TENANT_ID Azure AD tenant ID + M365_CLIENT_ID App registration client ID + M365_CLIENT_SECRET App registration client secret + """, + ) + parser.add_argument( + "--collector", "-c", + help="Collector ID to run (e.g., 'entra.roles.cloud_only_admins')", + ) + parser.add_argument( + "--output", "-o", + type=Path, + help="Output directory for JSON files", + ) + parser.add_argument( + "--list", "-l", + action="store_true", + help="List available collectors", + ) + parser.add_argument( + "--all", "-a", + action="store_true", + help="Test all collectors and report status", + ) + parser.add_argument( + "--verbose", "-v", + action="store_true", + help="Show verbose output including credentials (masked) and stack traces", + ) + parser.add_argument( + "--use-service", + type=str, + default=None, + metavar="URL", + help="Use PowerShell HTTP service instead of Docker (e.g., http://localhost:8001)", + ) + + args = parser.parse_args() + + if args.list: + list_collectors() + return + + if args.all: + asyncio.run(test_all_collectors(args.output, args.use_service)) + return + + if not args.collector: + parser.error("--collector is required (or use --list to see available collectors)") + + asyncio.run(test_collector(args.collector, args.output, args.verbose, args.use_service)) + + +if __name__ == "__main__": + main() diff --git a/engine/uv.lock b/engine/uv.lock new file mode 100644 index 00000000..0b7f1a2f --- /dev/null +++ b/engine/uv.lock @@ -0,0 +1,999 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "autoaudit-worker" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "celery", extra = ["redis"] }, + { name = "cryptography" }, + { name = "dnspython" }, + { name = "httpx" }, + { name = "msal" }, + { name = "psycopg2-binary" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "celery", extras = ["redis"], specifier = ">=5.3.0" }, + { name = "cryptography", specifier = ">=42.0.0" }, + { name = "dnspython", specifier = ">=2.4.0" }, + { name = "httpx", specifier = ">=0.26.0" }, + { name = "msal", specifier = ">=1.31.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.9" }, + { name = "pydantic", specifier = ">=2.5.0" }, + { name = "pydantic-settings", specifier = ">=2.1.0" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, +] + +[[package]] +name = "billiard" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, +] + +[[package]] +name = "celery" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "exceptiongroup" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzlocal" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/5f/b681ae3c89290d2ea6562ea96b40f5af6f6fc5f7743e2cd1a19e47721548/celery-5.6.0.tar.gz", hash = "sha256:641405206042d52ae460e4e9751a2e31b06cf80ab836fcf92e0b9311d7ea8113", size = 1712522, upload-time = "2025-11-30T17:39:46.282Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/4e/53a125038d6a814491a0ae3457435c13cf8821eb602292cf9db37ce35f62/celery-5.6.0-py3-none-any.whl", hash = "sha256:33cf01477b175017fc8f22c5ee8a65157591043ba8ca78a443fe703aa910f581", size = 444561, upload-time = "2025-11-30T17:39:44.314Z" }, +] + +[package.optional-dependencies] +redis = [ + { name = "kombu", extra = ["redis"] }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "kombu" +version = "5.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/05/749ada8e51718445d915af13f1d18bc4333848e8faa0cb234028a3328ec8/kombu-5.6.1.tar.gz", hash = "sha256:90f1febb57ad4f53ca327a87598191b2520e0c793c75ea3b88d98e3b111282e4", size = 471548, upload-time = "2025-11-25T11:07:33.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/d6/943cf84117cd9ddecf6e1707a3f712a49fc64abdb8ac31b19132871af1dd/kombu-5.6.1-py3-none-any.whl", hash = "sha256:b69e3f5527ec32fc5196028a36376501682973e9620d6175d1c3d4eaf7e95409", size = 214141, upload-time = "2025-11-25T11:07:31.54Z" }, +] + +[package.optional-dependencies] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "msal" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987, upload-time = "2025-09-22T23:05:47.294Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/f2/8e377d29c2ecf99f6062d35ea606b036e8800720eccfec5fe3dd672c2b24/psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", size = 3756506, upload-time = "2025-10-10T11:10:30.144Z" }, + { url = "https://files.pythonhosted.org/packages/24/cc/dc143ea88e4ec9d386106cac05023b69668bd0be20794c613446eaefafe5/psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", size = 3863943, upload-time = "2025-10-10T11:10:34.586Z" }, + { url = "https://files.pythonhosted.org/packages/8c/df/16848771155e7c419c60afeb24950b8aaa3ab09c0a091ec3ccca26a574d0/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", size = 4410873, upload-time = "2025-10-10T11:10:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/43/79/5ef5f32621abd5a541b89b04231fe959a9b327c874a1d41156041c75494b/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", size = 4468016, upload-time = "2025-10-10T11:10:43.319Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9b/d7542d0f7ad78f57385971f426704776d7b310f5219ed58da5d605b1892e/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", size = 4164996, upload-time = "2025-10-10T11:10:46.705Z" }, + { url = "https://files.pythonhosted.org/packages/14/ed/e409388b537fa7414330687936917c522f6a77a13474e4238219fcfd9a84/psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", size = 3981881, upload-time = "2025-10-30T02:54:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/bf/30/50e330e63bb05efc6fa7c1447df3e08954894025ca3dcb396ecc6739bc26/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", size = 3650857, upload-time = "2025-10-10T11:10:50.112Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e0/4026e4c12bb49dd028756c5b0bc4c572319f2d8f1c9008e0dad8cc9addd7/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", size = 3296063, upload-time = "2025-10-10T11:10:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/2c/34/eb172be293c886fef5299fe5c3fcf180a05478be89856067881007934a7c/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", size = 3043464, upload-time = "2025-10-30T02:55:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/18/1c/532c5d2cb11986372f14b798a95f2eaafe5779334f6a80589a68b5fcf769/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", size = 3345378, upload-time = "2025-10-10T11:11:01.039Z" }, + { url = "https://files.pythonhosted.org/packages/70/e7/de420e1cf16f838e1fa17b1120e83afff374c7c0130d088dba6286fcf8ea/psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", size = 2713904, upload-time = "2025-10-10T11:11:04.81Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "redis" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, + { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, + { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, + { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, + { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, + { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, + { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] diff --git a/engine/worker/__init__.py b/engine/worker/__init__.py new file mode 100644 index 00000000..de752eec --- /dev/null +++ b/engine/worker/__init__.py @@ -0,0 +1,5 @@ +"""AutoAudit Celery worker package.""" + +from worker.celery_app import celery_app + +__all__ = ["celery_app"] diff --git a/engine/worker/celery_app.py b/engine/worker/celery_app.py new file mode 100644 index 00000000..ee64d5f9 --- /dev/null +++ b/engine/worker/celery_app.py @@ -0,0 +1,35 @@ +"""Celery application configuration for AutoAudit worker.""" + +from celery import Celery + +from worker.config import settings + +# Create Celery app +celery_app = Celery( + "autoaudit", + broker=settings.REDIS_URL, + # No result backend - results are written directly to PostgreSQL +) + +# Configure Celery +celery_app.conf.update( + # Serialization + task_serializer="json", + accept_content=["json"], + result_serializer="json", + # Timezone + timezone="UTC", + enable_utc=True, + # Task tracking + task_track_started=True, + # Task routing + task_default_queue="autoaudit", + # Retry settings + task_acks_late=True, + task_reject_on_worker_lost=True, + # Concurrency (for gevent pool) + worker_concurrency=10, +) + +# Auto-discover tasks from the worker.tasks module +celery_app.autodiscover_tasks(["worker"]) diff --git a/engine/worker/config.py b/engine/worker/config.py new file mode 100644 index 00000000..b82ded1c --- /dev/null +++ b/engine/worker/config.py @@ -0,0 +1,38 @@ +"""Configuration for the Celery worker.""" + +import os + +from pydantic_settings import BaseSettings + + +class WorkerSettings(BaseSettings): + """Worker configuration loaded from environment variables.""" + + # Database + DATABASE_URL: str = "postgresql://autoaudit:autoaudit_dev_password@localhost:5432/autoaudit" + + # Redis (Celery broker) + REDIS_URL: str = "redis://localhost:6379/0" + + # OPA (Open Policy Agent) + OPA_URL: str = "http://localhost:8181" + + # Encryption key for decrypting credentials + ENCRYPTION_KEY: str = "" + + # Policies directory + POLICIES_DIR: str = os.path.join(os.path.dirname(__file__), "..", "policies") + + # PowerShell service URL (optional - if set, uses HTTP instead of Docker) + POWERSHELL_SERVICE_URL: str | None = None + + class Config: + env_file = ".env" + + +def get_settings() -> WorkerSettings: + """Get worker settings instance.""" + return WorkerSettings() + + +settings = get_settings() diff --git a/engine/worker/db.py b/engine/worker/db.py new file mode 100644 index 00000000..68cbeb58 --- /dev/null +++ b/engine/worker/db.py @@ -0,0 +1,358 @@ +"""Database access for the Celery worker. + +Uses synchronous SQLAlchemy since Celery tasks are synchronous. +For async operations within tasks, use asyncio.run(). + +--- +Extending for other cloud providers +--- + +Right now this module only handles M365 connections. The backend schema already +has tables for azure_connection, gcp_connection, and aws_connection - we just +haven't built the credential storage for those yet. + +When you add a new provider, you'll need to: + +1. Add a get__credentials() function that queries the relevant table + and decrypts any secrets. Look at how get_scan() joins to m365_connection + for the pattern. + +2. Update get_scan() to also pull from the new connection table. The scan table + has nullable FKs for each provider (azure_connection_id, gcp_connection_id, + aws_connection_id) - only one will be non-null per scan. + +3. Add a collector in /engine/collectors// that knows how to use + those credentials to call the provider's APIs. + +4. The task in tasks.py will need a branch to pick the right collector based on + which connection ID is populated on the scan. + +The encryption approach is the same across all providers - Fernet symmetric +encryption with the key from ENCRYPTION_KEY env var. See decrypt() below. +""" + +import json +from contextlib import contextmanager +from datetime import datetime +from decimal import Decimal +from typing import Generator + +from cryptography.fernet import Fernet +from sqlalchemy import create_engine, text +from sqlalchemy.orm import Session, sessionmaker + +from worker.config import settings + +# Convert async URL to sync URL for worker +sync_database_url = settings.DATABASE_URL.replace( + "postgresql+asyncpg://", "postgresql://" +) + +# Create sync engine and session factory +engine = create_engine(sync_database_url, pool_pre_ping=True) +SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False) + + +@contextmanager +def get_db_session() -> Generator[Session, None, None]: + """Get a database session context manager.""" + session = SessionLocal() + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() + + +# Fernet instance for decryption +_fernet: Fernet | None = None + + +def get_fernet() -> Fernet: + """Get or create Fernet instance for credential decryption.""" + global _fernet + if _fernet is None: + if not settings.ENCRYPTION_KEY: + raise ValueError("ENCRYPTION_KEY environment variable is required") + _fernet = Fernet(settings.ENCRYPTION_KEY.encode()) + return _fernet + + +def decrypt(ciphertext: str) -> str: + """Decrypt a ciphertext string.""" + if not ciphertext: + return "" + return get_fernet().decrypt(ciphertext.encode()).decode() + + +def get_scan(session: Session, scan_id: int) -> dict | None: + """Get scan details by ID. + + Returns a dict with scan data including the M365 connection credentials. + For other providers, you'd add similar joins here - see module docstring. + """ + result = session.execute( + text(""" + SELECT s.id, s.user_id, + s.m365_connection_id, s.azure_connection_id, + s.gcp_connection_id, s.aws_connection_id, + s.framework, s.benchmark, s.version, + s.status, s.started_at, s.finished_at, + s.compliance_score, s.total_controls, s.passed_count, + s.failed_count, s.skipped_count, s.error_count, s.notes, + c.tenant_id, c.client_id, c.encrypted_client_secret + FROM scan s + LEFT JOIN m365_connection c ON s.m365_connection_id = c.id + WHERE s.id = :scan_id + """), + {"scan_id": scan_id}, + ) + row = result.fetchone() + if not row: + return None + + return { + "id": row.id, + "user_id": row.user_id, + # Connection IDs - only one of these should be set per scan + "m365_connection_id": row.m365_connection_id, + "azure_connection_id": row.azure_connection_id, + "gcp_connection_id": row.gcp_connection_id, + "aws_connection_id": row.aws_connection_id, + # What we're scanning against + "framework": row.framework, + "benchmark": row.benchmark, + "version": row.version, + # Status and timing + "status": row.status, + "started_at": row.started_at, + "finished_at": row.finished_at, + # Results + "compliance_score": row.compliance_score, + "total_controls": row.total_controls, + "passed_count": row.passed_count, + "failed_count": row.failed_count, + "skipped_count": row.skipped_count, + "error_count": row.error_count, + "notes": row.notes, + # M365 credentials (decrypted) - add similar blocks for other providers + "tenant_id": row.tenant_id, + "client_id": row.client_id, + "client_secret": decrypt(row.encrypted_client_secret) if row.encrypted_client_secret else None, + } + + +def get_pending_scan_results(session: Session, scan_id: int) -> list[dict]: + """Get all scan results with status='pending' for a scan. + + Returns list of dicts with result details including control metadata. + """ + result = session.execute( + text(""" + SELECT id, scan_id, control_id, status, message, evidence + FROM scan_result + WHERE scan_id = :scan_id AND status = 'pending' + ORDER BY control_id + """), + {"scan_id": scan_id}, + ) + rows = result.fetchall() + return [ + { + "id": row.id, + "scan_id": row.scan_id, + "control_id": row.control_id, + "status": row.status, + "message": row.message, + "evidence": row.evidence, + } + for row in rows + ] + + +def update_scan_status( + session: Session, + scan_id: int, + status: str, + finished_at: datetime | None = None, + total_controls: int | None = None, + passed_count: int | None = None, + failed_count: int | None = None, + skipped_count: int | None = None, + error_count: int | None = None, + compliance_score: Decimal | None = None, + notes: str | None = None, +) -> None: + """Update scan status and optionally other fields.""" + updates = ["status = :status"] + params = {"scan_id": scan_id, "status": status} + + if finished_at is not None: + updates.append("finished_at = :finished_at") + params["finished_at"] = finished_at + if total_controls is not None: + updates.append("total_controls = :total_controls") + params["total_controls"] = total_controls + if passed_count is not None: + updates.append("passed_count = :passed_count") + params["passed_count"] = passed_count + if failed_count is not None: + updates.append("failed_count = :failed_count") + params["failed_count"] = failed_count + if skipped_count is not None: + updates.append("skipped_count = :skipped_count") + params["skipped_count"] = skipped_count + if error_count is not None: + updates.append("error_count = :error_count") + params["error_count"] = error_count + if compliance_score is not None: + updates.append("compliance_score = :compliance_score") + params["compliance_score"] = compliance_score + if notes is not None: + updates.append("notes = :notes") + params["notes"] = notes + + session.execute( + text(f"UPDATE scan SET {', '.join(updates)} WHERE id = :scan_id"), + params, + ) + + +def increment_scan_progress( + session: Session, + scan_id: int, + passed: bool, +) -> None: + """Increment the passed or failed count for a scan.""" + if passed: + session.execute( + text("UPDATE scan SET passed_count = passed_count + 1 WHERE id = :scan_id"), + {"scan_id": scan_id}, + ) + else: + session.execute( + text("UPDATE scan SET failed_count = failed_count + 1 WHERE id = :scan_id"), + {"scan_id": scan_id}, + ) + + +def increment_scan_error_count(session: Session, scan_id: int) -> None: + """Increment the error count for a scan.""" + session.execute( + text("UPDATE scan SET error_count = error_count + 1 WHERE id = :scan_id"), + {"scan_id": scan_id}, + ) + + +def increment_scan_skipped_count(session: Session, scan_id: int, amount: int = 1) -> None: + """Increment the skipped count for a scan.""" + session.execute( + text("UPDATE scan SET skipped_count = skipped_count + :amount WHERE id = :scan_id"), + {"scan_id": scan_id, "amount": amount}, + ) + + +def update_scan_result( + session: Session, + result_id: int, + status: str, + message: str | None = None, + evidence: dict | None = None, +) -> None: + """Update a scan result record. + + Args: + session: Database session + result_id: The scan_result.id to update + status: New status (passed, failed, error) + message: Human-readable result message + evidence: Details from OPA evaluation + """ + updates = ["status = :status", "updated_at = now()"] + params = {"result_id": result_id, "status": status} + + if message is not None: + updates.append("message = :message") + params["message"] = message + + if evidence is not None: + updates.append("evidence = CAST(:evidence AS jsonb)") + params["evidence"] = json.dumps(evidence) + + session.execute( + text(f"UPDATE scan_result SET {', '.join(updates)} WHERE id = :result_id"), + params, + ) + + +def finalize_scan_if_complete(session: Session, scan_id: int) -> bool: + """Check if all controls are evaluated and finalize scan if complete. + + This function uses SELECT FOR UPDATE to prevent race conditions when + multiple tasks complete simultaneously. + + Args: + session: Database session + scan_id: The scan ID to check + + Returns: + True if scan was finalized, False if still pending controls + """ + # Check if there are any pending controls remaining + result = session.execute( + text(""" + SELECT COUNT(*) as pending_count + FROM scan_result + WHERE scan_id = :scan_id AND status = 'pending' + """), + {"scan_id": scan_id}, + ) + pending_count = result.scalar() + + if pending_count > 0: + # Still have pending controls + return False + + # All controls complete - finalize the scan + # Use SELECT FOR UPDATE to prevent race condition + result = session.execute( + text(""" + SELECT id, status, passed_count, failed_count + FROM scan + WHERE id = :scan_id + FOR UPDATE + """), + {"scan_id": scan_id}, + ) + row = result.fetchone() + + if not row or row.status == "completed": + # Already finalized by another task + return False + + # Calculate compliance score + passed = row.passed_count or 0 + failed = row.failed_count or 0 + total = passed + failed + + if total > 0: + compliance_score = Decimal(str(round(passed / total * 100, 2))) + else: + compliance_score = Decimal("0.00") + + # Update scan to completed + session.execute( + text(""" + UPDATE scan + SET status = 'completed', + finished_at = now(), + compliance_score = :compliance_score + WHERE id = :scan_id + """), + {"scan_id": scan_id, "compliance_score": compliance_score}, + ) + + return True diff --git a/engine/worker/tasks.py b/engine/worker/tasks.py new file mode 100644 index 00000000..421f0c2b --- /dev/null +++ b/engine/worker/tasks.py @@ -0,0 +1,380 @@ +"""Celery tasks for compliance scanning.""" + +import asyncio +import json +from datetime import datetime +from decimal import Decimal +from pathlib import Path + +from worker.celery_app import celery_app +from worker.config import settings +from worker.db import ( + get_db_session, + get_scan, + get_pending_scan_results, + update_scan_status, + increment_scan_progress, + increment_scan_error_count, + increment_scan_skipped_count, + update_scan_result, + finalize_scan_if_complete, +) + + +def load_metadata(framework: str, benchmark: str, version: str) -> dict: + """Load control metadata from the policies directory. + + Args: + framework: Framework name (e.g., "cis") + benchmark: Benchmark slug (e.g., "microsoft-365-foundations") + version: Version (e.g., "v3.1.0") + + Returns: + The metadata dict containing controls list. + """ + metadata_path = Path(settings.POLICIES_DIR) / framework / benchmark / version / "metadata.json" + if not metadata_path.exists(): + raise FileNotFoundError(f"Metadata not found: {metadata_path}") + + with open(metadata_path) as f: + return json.load(f) + + +def get_control_metadata(metadata: dict, control_id: str) -> dict | None: + """Get control metadata by control_id from the metadata dict.""" + for control in metadata.get("controls", []): + if control["control_id"] == control_id: + return control + return None + + +@celery_app.task(name="worker.tasks.run_scan") +def run_scan(scan_id: int) -> dict: + """Orchestrator task: Dispatches control evaluation tasks. + + This task: + 1. Updates scan status to "running" + 2. Gets pending ScanResult records + 3. Dispatches evaluate_control tasks for each pending result + 4. Returns immediately (fire-and-forget) + + Each evaluate_control task writes results directly to PostgreSQL. + The last task to complete will finalize the scan. + + Args: + scan_id: The scan ID to process + + Returns: + Summary dict with dispatch info + """ + with get_db_session() as session: + # Get scan details + scan = get_scan(session, scan_id) + if not scan: + raise ValueError(f"Scan {scan_id} not found") + + # Update status to running + update_scan_status(session, scan_id, status="running") + session.commit() + + # Get pending scan results (controls that need to be evaluated) + pending_results = get_pending_scan_results(session, scan_id) + + total_pending = len(pending_results) + + if total_pending == 0: + # No controls to evaluate - scan was created with all controls skipped + with get_db_session() as session: + update_scan_status( + session, + scan_id, + status="completed", + finished_at=datetime.utcnow(), + compliance_score=Decimal("100.00"), + ) + session.commit() + return {"scan_id": scan_id, "status": "completed", "total_pending": 0} + + # Load metadata to get control details + metadata = load_metadata(scan["framework"], scan["benchmark"], scan["version"]) + + # Build credentials dict for passing to tasks + credentials = { + "tenant_id": scan["tenant_id"], + "client_id": scan["client_id"], + "client_secret": scan["client_secret"], + } + + # Dispatch all tasks for parallel execution (fire-and-forget) + dispatched = 0 + skipped = 0 + + for result in pending_results: + control = get_control_metadata(metadata, result["control_id"]) + if not control: + # Control not found in metadata (possible ID format mismatch) + with get_db_session() as session: + update_scan_result( + session, + result_id=result["id"], + status="error", + message=f"Control {result['control_id']} not found in metadata", + ) + increment_scan_error_count(session, scan_id) + session.commit() + continue + + # Check automation_status before dispatching + status = control.get("automation_status", "ready") + + if status == "ready": + # Verify collector exists before dispatching + if not control.get("data_collector_id"): + with get_db_session() as session: + update_scan_result( + session, + result_id=result["id"], + status="error", + message="Control marked ready but has no data_collector_id", + ) + increment_scan_error_count(session, scan_id) + session.commit() + continue + + evaluate_control.delay( + scan_id=scan_id, + result_id=result["id"], + control=control, + credentials=credentials, + framework=scan["framework"], + benchmark=scan["benchmark"], + version=scan["version"], + ) + dispatched += 1 + else: + # Skip non-ready controls (deferred, blocked, manual, not_started) + with get_db_session() as session: + update_scan_result( + session, + result_id=result["id"], + status="skipped", + message=f"Control {status}: {control.get('notes') or 'Not yet automatable'}", + ) + increment_scan_skipped_count(session, scan_id) + session.commit() + skipped += 1 + + # If no tasks were dispatched, finalize the scan immediately + # (all controls were skipped due to automation_status) + if dispatched == 0: + with get_db_session() as session: + finalize_scan_if_complete(session, scan_id) + session.commit() + return { + "scan_id": scan_id, + "status": "completed", + "dispatched": dispatched, + "skipped": skipped, + } + + # Return immediately - don't wait for results + # Each evaluate_control task will update PostgreSQL directly + # The last task to complete will finalize the scan + return { + "scan_id": scan_id, + "status": "running", + "dispatched": dispatched, + "skipped": skipped, + } + + +@celery_app.task( + name="worker.tasks.evaluate_control", + bind=True, + max_retries=3, + default_retry_delay=60, +) +def evaluate_control( + self, + scan_id: int, + result_id: int, + control: dict, + credentials: dict, + framework: str, + benchmark: str, + version: str, +) -> dict: + """Evaluate a single control. + + This task: + 1. Collects data using the appropriate collector + 2. Evaluates the policy using OPA + 3. Updates the ScanResult record with the outcome + 4. Updates scan progress counters + 5. If this was the last pending control, finalizes the scan + + Args: + scan_id: The scan ID + result_id: The scan_result.id to update + control: Control metadata dict from metadata.json + credentials: M365 credentials dict + framework: Framework name (e.g., "cis") + benchmark: Benchmark slug (e.g., "microsoft-365-foundations") + version: Version string (e.g., "v3.1.0") + + Returns: + Result dict with control evaluation outcome + """ + control_id = control["control_id"] + collector_id = control.get("data_collector_id") + policy_file = control.get("policy_file") + + try: + # Run async collector and OPA evaluation + result = asyncio.run( + _evaluate_control_async( + control_id=control_id, + collector_id=collector_id, + policy_file=policy_file, + credentials=credentials, + framework=framework, + benchmark=benchmark, + version=version, + ) + ) + + # Update database based on result + with get_db_session() as session: + if result.get("compliant", False): + # Control passed + update_scan_result( + session, + result_id=result_id, + status="passed", + message=result.get("message", "Control is compliant"), + evidence=result.get("details"), + ) + increment_scan_progress(session, scan_id, passed=True) + else: + # Control failed + update_scan_result( + session, + result_id=result_id, + status="failed", + message=result.get("message", "Control is non-compliant"), + evidence=result.get("details"), + ) + increment_scan_progress(session, scan_id, passed=False) + + # Check if this was the last control and finalize scan if complete + finalize_scan_if_complete(session, scan_id) + session.commit() + + return { + "control_id": control_id, + "compliant": result.get("compliant", False), + "message": result.get("message"), + } + + except Exception as exc: + # Retry on failure + try: + raise self.retry(exc=exc) + except self.MaxRetriesExceededError: + # Max retries exceeded - mark as error + with get_db_session() as session: + update_scan_result( + session, + result_id=result_id, + status="error", + message=f"Control evaluation failed after retries: {str(exc)}", + ) + increment_scan_error_count(session, scan_id) + + # Check if this was the last control and finalize scan if complete + finalize_scan_if_complete(session, scan_id) + session.commit() + return { + "control_id": control_id, + "compliant": None, + "error": str(exc), + } + + +async def _evaluate_control_async( + control_id: str, + collector_id: str, + policy_file: str, + credentials: dict, + framework: str, + benchmark: str, + version: str, +) -> dict: + """Async helper to collect data and evaluate policy. + + Args: + control_id: The control ID (e.g., "CIS-1.1.1") + collector_id: The data collector ID from registry + policy_file: The Rego policy filename (e.g., "1.1.1_admin_cloud_only.rego") + credentials: M365 credentials + framework: Framework name (e.g., "cis") + benchmark: Benchmark slug (e.g., "microsoft-365-foundations") + version: Version string (e.g., "v3.1.0") + + Returns: + OPA evaluation result + """ + # Import here to avoid circular imports + from collectors.registry import get_collector + from collectors.graph_client import GraphClient + from collectors.powershell_client import PowerShellClient + from opa_client import opa_client + + # Get collector + collector = get_collector(collector_id) + + # Determine client type based on collector_id prefix. + # + # Most Exchange and Compliance collectors require PowerShell, but a few Exchange + # collectors use Graph (e.g. domain metadata). + if collector_id.startswith(("exchange.", "compliance.")) and not collector_id.startswith( + "exchange.dns." + ): + client = PowerShellClient( + tenant_id=credentials["tenant_id"], + client_id=credentials["client_id"], + client_secret=credentials["client_secret"], + service_url=settings.POWERSHELL_SERVICE_URL, + ) + else: + # Entra and other collectors use Graph API + client = GraphClient( + tenant_id=credentials["tenant_id"], + client_id=credentials["client_id"], + client_secret=credentials["client_secret"], + ) + + # Collect data using the appropriate client + collected_data = await collector.collect(client) + + # Build OPA package path to match the Rego package declaration + # Rego package: "cis.microsoft_365_foundations.v3_1_0.control_1_1_1" + # OPA REST API path: "cis/microsoft_365_foundations/v3_1_0/control_1_1_1" + # + # Transform: + # - benchmark: "microsoft-365-foundations" -> "microsoft_365_foundations" + # - version: "v3.1.0" -> "v3_1_0" + # - control_id: "1.1.1" -> "control_1_1_1" + benchmark_normalized = benchmark.replace("-", "_") + version_normalized = version.replace(".", "_") + + # Convert control_id like "1.1.1" to "control_1_1_1" + control_suffix = control_id.replace(".", "_") + control_package = f"control_{control_suffix}" + + package_path = f"{framework}/{benchmark_normalized}/{version_normalized}/{control_package}" + + # Evaluate policy with OPA + result = await opa_client.evaluate_policy(package_path, collected_data) + + return result diff --git a/engine/worker_entrypoint.sh b/engine/worker_entrypoint.sh new file mode 100644 index 00000000..0c068ad2 --- /dev/null +++ b/engine/worker_entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +echo "AutoAudit Worker Container" +echo "==========================" +echo "Starting Celery worker with prefork pool..." +echo "" + +# Run Celery worker with prefork pool +# --pool=prefork: Multiple worker processes, each with isolated event loop +# --concurrency=4: 4 worker processes (adjust based on container resources) +# --loglevel=info: Standard logging level +# +# Note: We use prefork instead of gevent because the collectors use asyncio/httpx. +# Each prefork worker process has its own event loop, avoiding conflicts. +exec celery -A worker.celery_app worker --pool=prefork --concurrency=4 --loglevel=info diff --git a/env.example b/env.example new file mode 100644 index 00000000..74db8963 --- /dev/null +++ b/env.example @@ -0,0 +1,12 @@ +# AutoAudit local development environment variables +# For local dev, create a `.env` file (gitignored) +# +# Usage: +# cp env.example .env + +# Google SSO +GOOGLE_OAUTH_CLIENT_ID= 237734019606-8lft9r71d02ljcegsq4d6huglh8ke151.apps.googleusercontent.com +GOOGLE_OAUTH_CLIENT_SECRET= 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 + + + diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 00000000..c44733ca --- /dev/null +++ b/frontend/.env @@ -0,0 +1,4 @@ +# Backend API URL +VITE_API_URL=http://localhost:8000 + + diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 00000000..70974d24 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# Required: Backend API URL +VITE_API_URL=http://localhost:8000 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..1dd4dc6c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + AutoAudit + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 411ec4c1..e86de348 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "@fontsource/league-spartan": "^5.2.7", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", "chart.js": "^4.5.0", @@ -18,50 +17,15 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.1", - "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "tailwindcss": "^4.1.18" }, "devDependencies": { - "autoprefixer": "^10.4.21", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.17" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "license": "MIT" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "@vitejs/plugin-react": "^5.1.2", + "vite": "^7.3.0" } }, "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==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -73,30 +37,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.5", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "version": "7.28.5", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.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", @@ -111,59 +74,13 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/eslint-parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz", - "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==", - "license": "MIT", - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/@babel/eslint-parser/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.28.5", + "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@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" @@ -172,22 +89,9 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "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", @@ -200,113 +104,17 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.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==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "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-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", @@ -318,8 +126,7 @@ }, "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", @@ -333,87 +140,24 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, "engines": { "node": ">=6.9.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.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -421,47 +165,30 @@ }, "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==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.5", + "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -470,26 +197,23 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -498,16081 +222,1015 @@ "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "node_modules/@babel/runtime": { + "version": "7.28.4", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "node_modules/@babel/template": { + "version": "7.27.2", + "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "node_modules/@babel/traverse": { + "version": "7.28.5", + "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@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" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "node_modules/@babel/types": { + "version": "7.28.5", + "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", - "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-decorators": "^7.27.1" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", - "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", - "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", - "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", - "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-flow": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", - "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", - "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", - "license": "MIT", - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", - "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", - "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", - "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", - "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-dotall-regex": "^7.27.1", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", - "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.27.1", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", - "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "license": "MIT", - "engines": { - "node": ">=6.9.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==", - "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.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "license": "MIT" - }, - "node_modules/@csstools/normalize.css": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz", - "integrity": "sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ==", - "license": "CC0-1.0" - }, - "node_modules/@csstools/postcss-cascade-layers": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", - "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/selector-specificity": "^2.0.2", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-color-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", - "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", - "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", - "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", - "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", - "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-nested-calc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", - "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", - "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", - "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", - "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", - "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", - "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", - "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-unset-value": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", - "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", - "license": "CC0-1.0", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", - "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", - "license": "CC0-1.0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.10" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", - "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", - "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/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/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==", - "license": "Python-2.0" - }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@fontsource/league-spartan": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@fontsource/league-spartan/-/league-spartan-5.2.7.tgz", - "integrity": "sha512-OwYjMcJOGAGB680+4+Z47c/G5Ky8f8mT9Md6e8DB8/6fi1RGd/Ct5pUe3KT/lhUeNGUT8m5R4PKww47qtU8NHw==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.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==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/core": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", - "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", - "license": "MIT", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", - "micromatch": "^4.0.4", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", - "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/source-map/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "license": "MIT", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", - "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", - "license": "MIT", - "dependencies": { - "@jest/test-result": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/@jest/transform/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@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==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@kurkle/color": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", - "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "license": "MIT" - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "license": "MIT" - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "license": "MIT", - "dependencies": { - "eslint-scope": "5.1.1" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "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==", - "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==", - "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==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz", - "integrity": "sha512-tXDyE1/jzFsHXjhRZQ3hMl0IVhYe5qula43LDWIhVfjp9G/nT5OQY5AORVOrkEGAUltBJOfOWeETbmhm6kHhuQ==", - "license": "MIT", - "dependencies": { - "ansi-html": "^0.0.9", - "core-js-pure": "^3.23.3", - "error-stack-parser": "^2.0.6", - "html-entities": "^2.1.0", - "loader-utils": "^2.0.4", - "schema-utils": "^4.2.0", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "@types/webpack": "4.x || 5.x", - "react-refresh": ">=0.10.0 <1.0.0", - "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <5.0.0", - "webpack": ">=4.43.0 <6.0.0", - "webpack-dev-server": "3.x || 4.x || 5.x", - "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "0.x || 1.x" - }, - "peerDependenciesMeta": { - "@types/webpack": { - "optional": true - }, - "sockjs-client": { - "optional": true - }, - "type-fest": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - }, - "webpack-hot-middleware": { - "optional": true - }, - "webpack-plugin-serve": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-babel": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.10.4", - "@rollup/pluginutils": "^3.1.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", - "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "magic-string": "^0.25.7" - }, - "peerDependencies": { - "rollup": "^1.20.0 || ^2.0.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "license": "MIT", - "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/pluginutils/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "license": "MIT" - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "license": "MIT" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", - "license": "MIT" - }, - "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@surma/rollup-plugin-off-main-thread": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", - "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", - "license": "Apache-2.0", - "dependencies": { - "ejs": "^3.1.6", - "json5": "^2.2.0", - "magic-string": "^0.25.0", - "string.prototype.matchall": "^4.0.6" - } - }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", - "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", - "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", - "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", - "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", - "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", - "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-preset": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", - "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", - "license": "MIT", - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", - "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", - "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", - "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", - "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", - "@svgr/babel-plugin-transform-svg-component": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", - "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", - "license": "MIT", - "dependencies": { - "@svgr/plugin-jsx": "^5.5.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", - "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.12.6" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", - "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.12.3", - "@svgr/babel-preset": "^5.5.0", - "@svgr/hast-util-to-babel-ast": "^5.5.0", - "svg-parser": "^2.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-svgo": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", - "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", - "license": "MIT", - "dependencies": { - "cosmiconfig": "^7.0.0", - "deepmerge": "^4.2.2", - "svgo": "^1.2.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/webpack": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", - "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/plugin-transform-react-constant-elements": "^7.12.1", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.5", - "@svgr/core": "^5.5.0", - "@svgr/plugin-jsx": "^5.5.0", - "@svgr/plugin-svgo": "^5.5.0", - "loader-utils": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz", - "integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==", - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", - "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.12", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", - "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "license": "MIT" - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "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==", - "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==", - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "license": "MIT" - }, - "node_modules/@types/q": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", - "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==", - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "license": "MIT" - }, - "node_modules/@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", - "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "license": "BSD-3-Clause" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "license": "MIT", - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.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==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.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==", - "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/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", - "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "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==", - "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==", - "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-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "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==", - "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-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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==", - "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==", - "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==", - "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==", - "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.reduce": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.8.tgz", - "integrity": "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-array-method-boxes-properly": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "is-string": "^1.1.1" - }, - "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==", - "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==", - "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/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "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==", - "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.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", - "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==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", - "license": "MIT", - "dependencies": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-loader": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz", - "integrity": "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==", - "license": "MIT", - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.4", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-loader/node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/babel-plugin-named-asset-import": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", - "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.1.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-transform-react-remove-prop-types": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", - "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", - "license": "MIT" - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^27.5.1", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-react-app": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.1.0.tgz", - "integrity": "sha512-f9B1xMdnkCIqe+2dHrJsoQFRz7reChaAHE/65SdaykPklQqhme2WaC08oD3is77x9ff98/9EazAKFDZv5rFEQg==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.16.0", - "@babel/plugin-proposal-decorators": "^7.16.4", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", - "@babel/plugin-proposal-numeric-separator": "^7.16.0", - "@babel/plugin-proposal-optional-chaining": "^7.16.0", - "@babel/plugin-proposal-private-methods": "^7.16.0", - "@babel/plugin-proposal-private-property-in-object": "^7.16.7", - "@babel/plugin-transform-flow-strip-types": "^7.16.0", - "@babel/plugin-transform-react-display-name": "^7.16.0", - "@babel/plugin-transform-runtime": "^7.16.4", - "@babel/preset-env": "^7.16.4", - "@babel/preset-react": "^7.16.0", - "@babel/preset-typescript": "^7.16.0", - "@babel/runtime": "^7.16.3", - "babel-plugin-macros": "^3.1.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", - "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "license": "MIT" - }, - "node_modules/bfj": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", - "integrity": "sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==", - "license": "MIT", - "dependencies": { - "bluebird": "^3.7.2", - "check-types": "^11.2.3", - "hoopy": "^0.1.4", - "jsonpath": "^1.1.1", - "tryer": "^1.0.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "license": "ISC" - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "license": "BSD-2-Clause" - }, - "node_modules/browserslist": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", - "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", - "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": { - "caniuse-lite": "^1.0.30001737", - "electron-to-chromium": "^1.5.211", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "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==", - "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==", - "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==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "license": "MIT", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001739", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", - "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", - "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/case-sensitive-paths-webpack-plugin": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", - "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "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/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/chart.js": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", - "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", - "license": "MIT", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } - }, - "node_modules/check-types": { - "version": "11.2.3", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", - "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==", - "license": "MIT" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/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==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "license": "MIT" - }, - "node_modules/clean-css": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", - "license": "MIT", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "license": "MIT", - "dependencies": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/coa/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/coa/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/coa/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/coa/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "license": "MIT" - }, - "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==", - "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==", - "license": "MIT" - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "license": "MIT" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "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==", - "license": "MIT" - }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "license": "MIT" - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/core-js": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", - "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", - "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.1.tgz", - "integrity": "sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/css-blank-pseudo": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", - "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "bin": { - "css-blank-pseudo": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-declaration-sorter": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", - "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-has-pseudo": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", - "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "bin": { - "css-has-pseudo": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", - "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", - "license": "MIT", - "dependencies": { - "cssnano": "^5.0.6", - "jest-worker": "^27.0.2", - "postcss": "^8.3.5", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@parcel/css": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/css-prefers-color-scheme": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", - "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "license": "CC0-1.0", - "bin": { - "css-prefers-color-scheme": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "license": "MIT" - }, - "node_modules/css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-tree/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "license": "MIT" - }, - "node_modules/cssdb": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", - "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ], - "license": "CC0-1.0" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", - "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", - "license": "MIT", - "dependencies": { - "cssnano-preset-default": "^5.2.14", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-preset-default": { - "version": "5.2.14", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", - "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", - "license": "MIT", - "dependencies": { - "css-declaration-sorter": "^6.3.1", - "cssnano-utils": "^3.1.0", - "postcss-calc": "^8.2.3", - "postcss-colormin": "^5.3.1", - "postcss-convert-values": "^5.1.3", - "postcss-discard-comments": "^5.1.2", - "postcss-discard-duplicates": "^5.1.0", - "postcss-discard-empty": "^5.1.1", - "postcss-discard-overridden": "^5.1.0", - "postcss-merge-longhand": "^5.1.7", - "postcss-merge-rules": "^5.1.4", - "postcss-minify-font-values": "^5.1.0", - "postcss-minify-gradients": "^5.1.1", - "postcss-minify-params": "^5.1.4", - "postcss-minify-selectors": "^5.2.1", - "postcss-normalize-charset": "^5.1.0", - "postcss-normalize-display-values": "^5.1.0", - "postcss-normalize-positions": "^5.1.1", - "postcss-normalize-repeat-style": "^5.1.1", - "postcss-normalize-string": "^5.1.0", - "postcss-normalize-timing-functions": "^5.1.0", - "postcss-normalize-unicode": "^5.1.1", - "postcss-normalize-url": "^5.1.0", - "postcss-normalize-whitespace": "^5.1.1", - "postcss-ordered-values": "^5.1.3", - "postcss-reduce-initial": "^5.1.2", - "postcss-reduce-transforms": "^5.1.0", - "postcss-svgo": "^5.1.0", - "postcss-unique-selectors": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "license": "MIT", - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "license": "CC0-1.0" - }, - "node_modules/csso/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "license": "MIT" - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "license": "MIT", - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "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==", - "license": "BSD-2-Clause" - }, - "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "license": "MIT", - "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "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==", - "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==", - "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.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "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==", - "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-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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==", - "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/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT" - }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "license": "MIT", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, - "node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "license": "MIT" - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "license": "MIT", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "deprecated": "Use your platform's native DOMException instead", - "license": "MIT", - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=10" - } - }, - "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "license": "BSD-2-Clause" - }, - "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==", - "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/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "license": "MIT" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.213", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz", - "integrity": "sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q==", - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "license": "MIT", - "dependencies": { - "stackframe": "^1.3.4" - } - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "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-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "license": "MIT" - }, - "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==", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "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-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "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.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "license": "MIT" - }, - "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==", - "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==", - "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==", - "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==", - "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==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-react-app": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", - "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.3", - "@rushstack/eslint-patch": "^1.1.0", - "@typescript-eslint/eslint-plugin": "^5.5.0", - "@typescript-eslint/parser": "^5.5.0", - "babel-preset-react-app": "^10.0.1", - "confusing-browser-globals": "^1.0.11", - "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jest": "^25.3.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.27.1", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-testing-library": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.0" - } - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-flowtype": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", - "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", - "license": "BSD-3-Clause", - "dependencies": { - "lodash": "^4.17.21", - "string-natural-compare": "^3.0.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@babel/plugin-syntax-flow": "^7.14.5", - "@babel/plugin-transform-react-jsx": "^7.14.9", - "eslint": "^8.1.0" - } - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jest": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", - "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/experimental-utils": "^5.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, - "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==", - "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==", - "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": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.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==", - "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-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-testing-library": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", - "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^5.58.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6" - }, - "peerDependencies": { - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "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==", - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", - "license": "MIT", - "dependencies": { - "@types/eslint": "^7.29.0 || ^8.4.1", - "jest-worker": "^28.0.2", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/eslint/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==", - "license": "Python-2.0" - }, - "node_modules/eslint/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==", - "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/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/eslint/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==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/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==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/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==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "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==", - "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==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "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.8" - }, - "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==", - "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==", - "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==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/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==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 0.4.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==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "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==", - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/form-data": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", - "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-monkey": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", - "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", - "license": "Unlicense" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "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==", - "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==", - "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==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "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==", - "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-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "license": "ISC" - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "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/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "license": "MIT", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "license": "MIT", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "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==", - "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/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "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==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "license": "MIT" - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "license": "MIT", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "license": "MIT" - }, - "node_modules/harmony-reflect": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", - "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", - "license": "(Apache-2.0 OR MPL-1.1)" - }, - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hoopy": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", - "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^1.0.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "license": "MIT" - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", - "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", - "license": "MIT", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "license": "MIT", - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "license": "ISC" - }, - "node_modules/identity-obj-proxy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", - "license": "MIT", - "dependencies": { - "harmony-reflect": "^1.4.6" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "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/import-fresh/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==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "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==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "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==", - "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-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "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-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "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==", - "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==", - "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==", - "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-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "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==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "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==", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "license": "MIT" - }, - "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==", - "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==", - "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==", - "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-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "license": "MIT" - }, - "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==", - "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-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "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==", - "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==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "license": "MIT" - }, - "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==", - "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==", - "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==", - "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/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "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/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", - "license": "MIT", - "dependencies": { - "@jest/core": "^27.5.1", - "import-local": "^3.0.2", - "jest-cli": "^27.5.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", - "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "execa": "^5.0.0", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", - "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "license": "MIT", - "dependencies": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", - "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", - "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", - "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-jasmine2": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", - "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-leak-detector": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", - "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", - "license": "MIT", - "dependencies": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", - "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", - "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", - "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", - "license": "MIT", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", - "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "leven": "^3.1.0", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-watch-typeahead": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", - "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.1", - "chalk": "^4.0.0", - "jest-regex-util": "^28.0.0", - "jest-watcher": "^28.0.0", - "slash": "^4.0.0", - "string-length": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "jest": "^27.0.0 || ^28.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", - "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", - "license": "MIT", - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", - "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", - "license": "MIT", - "dependencies": { - "@jest/console": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/types": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", - "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-message-util/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "license": "MIT", - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "license": "MIT", - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", - "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", - "license": "MIT", - "dependencies": { - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^28.1.3", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/pretty-format": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", - "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^28.1.3", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "node_modules/jest-watch-typeahead/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", - "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", - "license": "MIT", - "dependencies": { - "char-regex": "^2.0.0", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz", - "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/jest-watcher": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", - "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", - "license": "MIT", - "dependencies": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.5.1", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "license": "MIT", - "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "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==", - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, - "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==", - "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==", - "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==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpath": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", - "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", - "license": "MIT", - "dependencies": { - "esprima": "1.2.2", - "static-eval": "2.0.2", - "underscore": "1.12.1" - } - }, - "node_modules/jsonpath/node_modules/esprima": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", - "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/launch-editor": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", - "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", - "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "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==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "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==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.542.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz", - "integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "license": "MIT", - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "license": "CC0-1.0" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "license": "Unlicense", - "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", - "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "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==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "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/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "license": "MIT" - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", - "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==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "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==", - "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==", - "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==", - "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==", - "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==", - "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.getownpropertydescriptors": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz", - "integrity": "sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==", - "license": "MIT", - "dependencies": { - "array.prototype.reduce": "^1.0.6", - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "gopd": "^1.0.1", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.8" - }, - "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==", - "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==", - "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/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "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==", - "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": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "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==", - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "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==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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==", - "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==", - "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/postcss-attribute-case-insensitive": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", - "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-browser-comments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", - "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", - "license": "CC0-1.0", - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "browserslist": ">=4", - "postcss": ">=8" - } - }, - "node_modules/postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=7.6.0" - }, - "peerDependencies": { - "postcss": "^8.4.6" - } - }, - "node_modules/postcss-color-functional-notation": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", - "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-color-hex-alpha": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", - "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-rebeccapurple": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", - "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-colormin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", - "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-convert-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", - "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-custom-media": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", - "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-custom-properties": { - "version": "12.1.11", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", - "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-custom-selectors": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", - "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", - "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-discard-comments": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", - "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-empty": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-double-position-gradients": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", - "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-env-function": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", - "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-flexbugs-fixes": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.1.4" - } - }, - "node_modules/postcss-focus-visible": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", - "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-within": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", - "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-gap-properties": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", - "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", - "license": "CC0-1.0", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-image-set-function": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", - "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-initial": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-lab-function": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", - "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/postcss-loader": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", - "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", - "license": "MIT", - "dependencies": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.5", - "semver": "^7.3.5" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-logical": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "license": "CC0-1.0", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-media-minmax": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", - "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-rules": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", - "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.1.0", - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", - "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", - "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", - "license": "MIT", - "dependencies": { - "colord": "^2.9.1", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-params": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", - "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", - "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nesting": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", - "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-normalize": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", - "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/normalize.css": "*", - "postcss-browser-comments": "^4", - "sanitize.css": "*" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "browserslist": ">= 4", - "postcss": ">= 8" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", - "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", - "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", - "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-string": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", - "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", - "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", - "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", - "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", - "license": "MIT", - "dependencies": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", - "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-opacity-percentage": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", - "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], - "license": "MIT", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-ordered-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", - "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", - "license": "MIT", - "dependencies": { - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-overflow-shorthand": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", - "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8" - } - }, - "node_modules/postcss-place": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", - "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-preset-env": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", - "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-cascade-layers": "^1.1.1", - "@csstools/postcss-color-function": "^1.1.1", - "@csstools/postcss-font-format-keywords": "^1.0.1", - "@csstools/postcss-hwb-function": "^1.0.2", - "@csstools/postcss-ic-unit": "^1.0.1", - "@csstools/postcss-is-pseudo-class": "^2.0.7", - "@csstools/postcss-nested-calc": "^1.0.0", - "@csstools/postcss-normalize-display-values": "^1.0.1", - "@csstools/postcss-oklab-function": "^1.1.1", - "@csstools/postcss-progressive-custom-properties": "^1.3.0", - "@csstools/postcss-stepped-value-functions": "^1.0.1", - "@csstools/postcss-text-decoration-shorthand": "^1.0.0", - "@csstools/postcss-trigonometric-functions": "^1.0.2", - "@csstools/postcss-unset-value": "^1.0.2", - "autoprefixer": "^10.4.13", - "browserslist": "^4.21.4", - "css-blank-pseudo": "^3.0.3", - "css-has-pseudo": "^3.0.4", - "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^7.1.0", - "postcss-attribute-case-insensitive": "^5.0.2", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.4", - "postcss-color-hex-alpha": "^8.0.4", - "postcss-color-rebeccapurple": "^7.1.1", - "postcss-custom-media": "^8.0.2", - "postcss-custom-properties": "^12.1.10", - "postcss-custom-selectors": "^6.0.3", - "postcss-dir-pseudo-class": "^6.0.5", - "postcss-double-position-gradients": "^3.1.2", - "postcss-env-function": "^4.0.6", - "postcss-focus-visible": "^6.0.4", - "postcss-focus-within": "^5.0.4", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^3.0.5", - "postcss-image-set-function": "^4.0.7", - "postcss-initial": "^4.0.1", - "postcss-lab-function": "^4.2.1", - "postcss-logical": "^5.0.4", - "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.2.0", - "postcss-opacity-percentage": "^1.1.2", - "postcss-overflow-shorthand": "^3.0.4", - "postcss-page-break": "^3.0.4", - "postcss-place": "^7.0.5", - "postcss-pseudo-class-any-link": "^7.1.6", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", - "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", - "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", - "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", - "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", - "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/postcss-svgo/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/postcss-svgo/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "license": "CC0-1.0" - }, - "node_modules/postcss-svgo/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postcss-svgo/node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "license": "MIT", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", - "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/promise": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", - "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", - "license": "MIT", - "dependencies": { - "asap": "~2.0.6" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/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==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", - "license": "MIT", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "license": "MIT" - }, - "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==", - "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/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "license": "MIT", - "dependencies": { - "performance-now": "^2.1.0" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-app-polyfill": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", - "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", - "license": "MIT", - "dependencies": { - "core-js": "^3.19.2", - "object-assign": "^4.1.1", - "promise": "^8.1.0", - "raf": "^3.4.1", - "regenerator-runtime": "^0.13.9", - "whatwg-fetch": "^3.6.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/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==", - "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/react-dev-utils/node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/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==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/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==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/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==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.1" - } - }, - "node_modules/react-error-overlay": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz", - "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", - "license": "MIT" - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", - "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.1.tgz", - "integrity": "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.1.tgz", - "integrity": "sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==", - "license": "MIT", - "dependencies": { - "react-router": "7.9.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/react-scripts": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", - "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.16.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", - "@svgr/webpack": "^5.5.0", - "babel-jest": "^27.4.2", - "babel-loader": "^8.2.3", - "babel-plugin-named-asset-import": "^0.3.8", - "babel-preset-react-app": "^10.0.1", - "bfj": "^7.0.2", - "browserslist": "^4.18.1", - "camelcase": "^6.2.1", - "case-sensitive-paths-webpack-plugin": "^2.4.0", - "css-loader": "^6.5.1", - "css-minimizer-webpack-plugin": "^3.2.0", - "dotenv": "^10.0.0", - "dotenv-expand": "^5.1.0", - "eslint": "^8.3.0", - "eslint-config-react-app": "^7.0.1", - "eslint-webpack-plugin": "^3.1.1", - "file-loader": "^6.2.0", - "fs-extra": "^10.0.0", - "html-webpack-plugin": "^5.5.0", - "identity-obj-proxy": "^3.0.0", - "jest": "^27.4.3", - "jest-resolve": "^27.4.2", - "jest-watch-typeahead": "^1.0.0", - "mini-css-extract-plugin": "^2.4.5", - "postcss": "^8.4.4", - "postcss-flexbugs-fixes": "^5.0.2", - "postcss-loader": "^6.2.1", - "postcss-normalize": "^10.0.1", - "postcss-preset-env": "^7.0.1", - "prompts": "^2.4.2", - "react-app-polyfill": "^3.0.0", - "react-dev-utils": "^12.0.1", - "react-refresh": "^0.11.0", - "resolve": "^1.20.0", - "resolve-url-loader": "^4.0.0", - "sass-loader": "^12.3.0", - "semver": "^7.3.5", - "source-map-loader": "^3.0.0", - "style-loader": "^3.3.1", - "tailwindcss": "^3.0.2", - "terser-webpack-plugin": "^5.2.5", - "webpack": "^5.64.4", - "webpack-dev-server": "^4.6.0", - "webpack-manifest-plugin": "^4.0.2", - "workbox-webpack-plugin": "^6.4.1" - }, - "bin": { - "react-scripts": "bin/react-scripts.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - }, - "peerDependencies": { - "react": ">= 16", - "typescript": "^3.2.1 || ^4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "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/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "license": "MIT" - }, - "node_modules/regex-parser": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", - "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", - "license": "MIT" - }, - "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==", - "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/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", - "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.0.2" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "license": "MIT", - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "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-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-url-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", - "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", - "license": "MIT", - "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^7.0.35", - "source-map": "0.6.1" - }, - "engines": { - "node": ">=8.9" - }, - "peerDependencies": { - "rework": "1.0.1", - "rework-visit": "1.0.0" - }, - "peerDependenciesMeta": { - "rework": { - "optional": true - }, - "rework-visit": { - "optional": true - } - } - }, - "node_modules/resolve-url-loader/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/resolve-url-loader/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "license": "ISC" - }, - "node_modules/resolve-url-loader/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/resolve-url-loader/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve.exports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", - "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.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==", - "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==", - "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-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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/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==", - "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==", - "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/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sanitize.css": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", - "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==", - "license": "CC0-1.0" - }, - "node_modules/sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "license": "MIT", - "dependencies": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "license": "ISC" - }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "license": "MIT" - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "license": "MIT", - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "license": "ISC" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, - "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==", - "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==", - "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==", - "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/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "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==", - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "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==", - "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==", - "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==", - "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/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "license": "MIT", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "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/source-map-loader": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", - "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", - "license": "MIT", - "dependencies": { - "abab": "^2.0.5", - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "license": "MIT" - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", - "license": "MIT" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "license": "MIT" - }, - "node_modules/static-eval": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", - "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", - "license": "MIT", - "dependencies": { - "escodegen": "^1.8.1" - } - }, - "node_modules/static-eval/node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/static-eval/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/static-eval/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-eval/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-natural-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", - "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", - "license": "MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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/stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "license": "BSD-2-Clause", - "dependencies": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", - "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/stylehacks": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", - "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/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==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "license": "MIT" - }, - "node_modules/svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=4.0.0" + "node": ">=18" } }, - "node_modules/svgo/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/svgo/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/svgo/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/svgo/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/svgo/node_modules/css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "node_modules/svgo/node_modules/css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "license": "BSD-2-Clause", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/svgo/node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/svgo/node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" + "node": ">=18" } }, - "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "license": "BSD-2-Clause" - }, - "node_modules/svgo/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.8.0" + "node": ">=18" } }, - "node_modules/svgo/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "~1.0.0" + "node": ">=18" } }, - "node_modules/svgo/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "license": "MIT" - }, - "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" + "node": ">=18" } }, - "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=18" } }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/tempy": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", - "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "license": "(MIT OR CC0-1.0)", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" + "node": ">=18" } }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=0.8" + "node": ">=18" } }, - "node_modules/throat": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", - "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", - "license": "MIT" - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "license": "MIT" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "license": "BSD-3-Clause" - }, - "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==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 4.0.0" + "node": ">=18" } }, - "node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "punycode": "^2.1.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/tryer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", - "license": "MIT" - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "license": "Apache-2.0" + "node_modules/@fontsource/league-spartan": { + "version": "5.2.8", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } }, - "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==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, "license": "MIT", "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "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==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/tsconfig-paths/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==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6.0.0" } }, - "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/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" + "node_modules/@kurkle/color": { + "version": "0.3.4", + "license": "MIT" }, - "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==", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "dev": true, + "license": "MIT" }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "engines": { - "node": ">=4" - } + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.5", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.5", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } + "optional": true, + "os": [ + "win32" + ] }, - "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==", + "node_modules/@testing-library/dom": { + "version": "10.4.1", "license": "MIT", + "peer": true, "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "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==", + "node_modules/@testing-library/react": { + "version": "16.3.1", "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" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "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==", + "node_modules/@testing-library/user-event": { + "version": "13.5.0", "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" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">= 0.4" + "node": ">=10", + "npm": ">=6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" } }, - "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==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "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" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "dev": true, "license": "MIT", "dependencies": { - "is-typedarray": "^1.0.0" + "@babel/types": "^7.0.0" } }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.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==", + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "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" + "@babel/types": "^7.28.2" } }, - "node_modules/underscore": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", - "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, "license": "MIT" }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "dev": true, "license": "MIT", "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "license": "MIT", - "engines": { - "node": ">=4" + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "node_modules/ansi-regex": { + "version": "5.0.1", "license": "MIT", - "dependencies": { - "crypto-random-string": "^2.0.0" - }, "engines": { "node": ">=8" } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/ansi-styles": { + "version": "5.2.0", "license": "MIT", "engines": { - "node": ">= 10.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node_modules/aria-query": { + "version": "5.3.0", + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" } }, - "node_modules/unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", - "license": "MIT" - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "license": "MIT", - "engines": { - "node": ">=4", - "yarn": "*" + "node_modules/baseline-browser-mapping": { + "version": "2.9.9", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "node_modules/browserslist": { + "version": "4.28.1", + "dev": true, "funding": [ { "type": "opencollective", @@ -16588,1049 +1246,570 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" + "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": { - "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==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" + "browserslist": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", "engines": { - "node": ">= 0.4.0" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "dev": true, + "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/v8-to-istanbul": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", - "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", - "license": "ISC", + "node_modules/chart.js": { + "version": "4.5.1", + "license": "MIT", "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" + "@kurkle/color": "^0.3.0" }, "engines": { - "node": ">=10.12.0" + "pnpm": ">=8" } }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, "license": "MIT" }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "node_modules/cookie": { + "version": "1.1.1", "license": "MIT", "engines": { - "node": ">= 0.8" - } - }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", - "license": "MIT", - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "license": "MIT", - "dependencies": { - "xml-name-validator": "^3.0.0" + "node": ">=18" }, - "engines": { - "node": ">=10" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "node_modules/debug": { + "version": "4.4.3", + "dev": true, "license": "MIT", "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" + "ms": "^2.1.3" }, "engines": { - "node": ">=10.13.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "node_modules/dequal": { + "version": "2.0.3", "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" + "engines": { + "node": ">=6" } }, - "node_modules/web-vitals": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", - "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==", - "license": "Apache-2.0" + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "license": "MIT" }, - "node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=10.4" - } + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "dev": true, + "license": "ISC" }, - "node_modules/webpack": { - "version": "5.101.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", - "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "node_modules/esbuild": { + "version": "0.27.2", + "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.3.3" - }, "bin": { - "webpack": "bin/webpack.js" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=18" }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, - "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, "license": "MIT", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "node": ">=6" } }, - "node_modules/webpack-dev-server": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", - "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.4", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=12.0.0" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { + "picomatch": { "optional": true } } }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/webpack-manifest-plugin": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", - "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, "license": "MIT", - "dependencies": { - "tapable": "^2.0.0", - "webpack-sources": "^2.2.0" - }, "engines": { - "node": ">=12.22.0" - }, - "peerDependencies": { - "webpack": "^4.44.2 || ^5.47.0" + "node": ">=6.9.0" } }, - "node_modules/webpack-manifest-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" }, - "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, "license": "MIT", - "dependencies": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=10.13.0" + "node": ">=6" } }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "node_modules/json5": { + "version": "2.2.3", + "dev": true, "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "bin": { + "json5": "lib/cli.js" }, "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" + "node": ">=6" } }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "license": "Apache-2.0", + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" + "yallist": "^3.0.2" } }, - "node_modules/whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "license": "MIT", - "dependencies": { - "iconv-lite": "0.4.24" + "node_modules/lucide-react": { + "version": "0.542.0", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/lz-string": { + "version": "1.5.0", "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" + "bin": { + "lz-string": "bin/bin.js" } }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "node_modules/ms": { + "version": "2.1.3", + "dev": true, "license": "MIT" }, - "node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, "bin": { - "node-which": "bin/node-which" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">= 8" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/which-boxed-primitive": { + "node_modules/node-releases": { + "version": "2.0.27", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "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" - } + "license": "ISC" }, - "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==", + "node_modules/picomatch": { + "version": "4.0.3", + "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" - }, + "peer": true, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "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==", + "node_modules/postcss": { + "version": "8.5.6", + "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": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^10 || ^12 || >=14" } }, - "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==", + "node_modules/pretty-format": { + "version": "27.5.1", "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" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "node_modules/react": { + "version": "19.2.3", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/workbox-background-sync": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", - "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", + "node_modules/react-dom": { + "version": "19.2.3", "license": "MIT", + "peer": true, "dependencies": { - "idb": "^7.0.1", - "workbox-core": "6.6.0" + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" } }, - "node_modules/workbox-broadcast-update": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", - "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", - "license": "MIT", - "dependencies": { - "workbox-core": "6.6.0" - } + "node_modules/react-is": { + "version": "17.0.2", + "license": "MIT" }, - "node_modules/workbox-build": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", - "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", + "node_modules/react-refresh": { + "version": "0.18.0", + "dev": true, "license": "MIT", - "dependencies": { - "@apideck/better-ajv-errors": "^0.3.1", - "@babel/core": "^7.11.1", - "@babel/preset-env": "^7.11.0", - "@babel/runtime": "^7.11.2", - "@rollup/plugin-babel": "^5.2.0", - "@rollup/plugin-node-resolve": "^11.2.1", - "@rollup/plugin-replace": "^2.4.1", - "@surma/rollup-plugin-off-main-thread": "^2.2.3", - "ajv": "^8.6.0", - "common-tags": "^1.8.0", - "fast-json-stable-stringify": "^2.1.0", - "fs-extra": "^9.0.1", - "glob": "^7.1.6", - "lodash": "^4.17.20", - "pretty-bytes": "^5.3.0", - "rollup": "^2.43.1", - "rollup-plugin-terser": "^7.0.0", - "source-map": "^0.8.0-beta.0", - "stringify-object": "^3.3.0", - "strip-comments": "^2.0.1", - "tempy": "^0.6.0", - "upath": "^1.2.0", - "workbox-background-sync": "6.6.0", - "workbox-broadcast-update": "6.6.0", - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-google-analytics": "6.6.0", - "workbox-navigation-preload": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-range-requests": "6.6.0", - "workbox-recipes": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0", - "workbox-streams": "6.6.0", - "workbox-sw": "6.6.0", - "workbox-window": "6.6.0" - }, "engines": { - "node": ">=10.0.0" + "node": ">=0.10.0" } }, - "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", - "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "node_modules/react-router": { + "version": "7.11.0", "license": "MIT", "dependencies": { - "json-schema": "^0.4.0", - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, "engines": { - "node": ">=10" + "node": ">=20.0.0" }, "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/workbox-build/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "react": ">=18", + "react-dom": ">=18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, - "node_modules/workbox-build/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/react-router-dom": { + "version": "7.11.0", "license": "MIT", "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "react-router": "7.11.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/workbox-build/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/workbox-build/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "deprecated": "The work that was done in this beta branch won't be included in future versions", - "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" + "node": ">=20.0.0" }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/workbox-build/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/workbox-build/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "license": "BSD-2-Clause" - }, - "node_modules/workbox-build/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/workbox-cacheable-response": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", - "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", - "deprecated": "workbox-background-sync@6.6.0", + "node_modules/rollup": { + "version": "4.53.5", + "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", + "fsevents": "~2.3.2" } }, - "node_modules/workbox-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", - "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==", + "node_modules/scheduler": { + "version": "0.27.0", "license": "MIT" }, - "node_modules/workbox-expiration": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", - "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", - "license": "MIT", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-google-analytics": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", - "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", - "deprecated": "It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained", - "license": "MIT", - "dependencies": { - "workbox-background-sync": "6.6.0", - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-navigation-preload": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", - "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", - "license": "MIT", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-precaching": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", - "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", - "license": "MIT", - "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-range-requests": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", - "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", - "license": "MIT", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-recipes": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", - "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", - "license": "MIT", - "dependencies": { - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-routing": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", - "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", - "license": "MIT", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-strategies": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", - "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", - "license": "MIT", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-streams": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", - "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", - "license": "MIT", - "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0" + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/workbox-sw": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", - "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==", + "node_modules/set-cookie-parser": { + "version": "2.7.2", "license": "MIT" }, - "node_modules/workbox-webpack-plugin": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz", - "integrity": "sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A==", - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "^2.1.0", - "pretty-bytes": "^5.4.1", - "upath": "^1.2.0", - "webpack-sources": "^1.4.3", - "workbox-build": "6.6.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": "^4.4.0 || ^5.9.0" - } - }, - "node_modules/workbox-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "license": "MIT", - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "node_modules/workbox-window": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", - "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==", - "license": "MIT", - "dependencies": { - "@types/trusted-types": "^2.0.2", - "workbox-core": "6.6.0" - } + "node_modules/tailwindcss": { + "version": "4.1.18", + "license": "MIT" }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "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": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, - "engines": { - "node": ">=10" + "bin": { + "update-browserslist-db": "cli.js" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "node_modules/vite": { + "version": "7.3.0", + "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=8.3.0" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "bufferutil": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { "optional": true }, - "utf-8-validate": { + "yaml": { "optional": true } } }, - "node_modules/xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "license": "Apache-2.0" - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "license": "MIT" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "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/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/frontend/package.json b/frontend/package.json index 964a2701..5311fae3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,28 +5,18 @@ "dependencies": { "@fontsource/league-spartan": "^5.2.7", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", "chart.js": "^4.5.0", "lucide-react": "^0.542.0", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.1", - "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "react-router-dom": "^7.9.1" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] + "start": "vite", + "build": "vite build", + "preview": "vite preview" }, "browserslist": { "production": [ @@ -41,8 +31,8 @@ ] }, "devDependencies": { - "autoprefixer": "^10.4.21", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.17" + "@vitejs/plugin-react": "^5.1.2", + "vite": "^7.3.0", + "tailwindcss": "^4.1.18" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 96bb01e7..00000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/frontend/public/AutoAudit.webp b/frontend/public/AutoAudit.webp new file mode 100644 index 00000000..0e9f9495 Binary files /dev/null and b/frontend/public/AutoAudit.webp differ diff --git a/frontend/public/bg.webp b/frontend/public/bg.webp new file mode 100644 index 00000000..8ba4f209 Binary files /dev/null and b/frontend/public/bg.webp differ diff --git a/frontend/public/index.html b/frontend/public/index.html deleted file mode 100644 index aa069f27..00000000 --- a/frontend/public/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - React App - - - -
- - - diff --git a/frontend/src/App.js b/frontend/src/App.js deleted file mode 100644 index aba1b3b0..00000000 --- a/frontend/src/App.js +++ /dev/null @@ -1,185 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Routes, Route, useLocation, useNavigate } from 'react-router-dom'; - -// Dashboard Components -import Sidebar from './components/Sidebar'; -import Dashboard from './pages/Dashboard'; -import Evidence from './pages/Evidence'; -import StyleGuide from './pages/StyleGuide'; - -// Authentication & Landing Components -import LandingPage from './pages/Landing/LandingPage'; -import AboutUs from './pages/Landing/AboutUs'; -import LoginPage from './pages/Auth/LoginPage'; -import SignUpPage from './pages/Auth/SignUpPage'; - -// Styles -import './styles/global.css'; - -// Protected Route Component -const ProtectedRoute = ({ children, isAuthenticated }) => { - const navigate = useNavigate(); - - useEffect(() => { - if (!isAuthenticated) { - navigate('/login'); - } - }, [isAuthenticated, navigate]); - - return isAuthenticated ? children : null; -}; - -// Dashboard Layout Component (with sidebar) -const DashboardLayout = ({ children, sidebarWidth, isDarkMode, onThemeToggle }) => { - return ( - <> - {}} isDarkMode={isDarkMode} /> - {React.cloneElement(children, { sidebarWidth, isDarkMode, onThemeToggle })} - - ); -}; - -function App() { - // Authentication state - const [isAuthenticated, setIsAuthenticated] = useState(false); - - // Dashboard state - const [sidebarWidth, setSidebarWidth] = useState(220); - const [isDarkMode, setIsDarkMode] = useState(true); - - const location = useLocation(); - const navigate = useNavigate(); - - // Theme management - useEffect(() => { - localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); - const root = document.documentElement; - if (isDarkMode) { - root.classList.remove('light'); - } else { - root.classList.add('light'); - } - }, [isDarkMode]); - - // Authentication handlers - const handleUserLogin = () => { - setIsAuthenticated(true); - navigate('/dashboard'); - }; - - const handleUserLogout = () => { - setIsAuthenticated(false); - navigate('/'); - }; - - const handleSignUp = (signUpData) => { - console.log("Sign up data:", signUpData); - navigate('/login'); - }; - - const handleThemeToggle = () => { - setIsDarkMode(!isDarkMode); - }; - - const handleSidebarWidthChange = (width) => { - setSidebarWidth(width); - }; - - // Check if current route should show sidebar - const isDashboardRoute = ['/dashboard', '/evidence-scanner', '/styleguide'].includes(location.pathname); - - return ( -
- - {/* Public Routes */} - navigate('/login')} - onAboutClick={() => navigate('/about')} - /> - } - /> - - navigate('/')} /> - } - /> - - navigate('/signup')} - /> - } - /> - - navigate('/login')} - /> - } - /> - - {/* Protected Dashboard Routes */} - - - - - - } - /> - - - - - - - } - /> - - } - /> - - {/* Fallback route */} - navigate('/login')} - onAboutClick={() => navigate('/about')} - /> - } - /> - -
- ); -} - -export default App; \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 00000000..06f9cf55 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,319 @@ +import React, { useState, useEffect } from 'react'; +import { Routes, Route, useLocation, useNavigate } from 'react-router-dom'; + +// Dashboard Components +import Sidebar from './components/Sidebar'; +import Dashboard from './pages/Dashboard'; +import Evidence from './pages/Evidence'; +import SettingsPage from './pages/SettingsPage'; +import AccountPage from './pages/AccountPage'; +import StyleGuide from './pages/StyleGuide'; +import ConnectionsPage from './pages/Connections/ConnectionsPage'; +import ScansPage from './pages/Scans/ScansPage'; +import ScanDetailPage from './pages/Scans/ScanDetailPage'; + +// Authentication & Landing Components +import LandingPage from './pages/Landing/LandingPage'; +import AboutUs from './pages/Landing/AboutUs'; +import ContactPage from './pages/Contact/ContactPage'; +import LoginPage from './pages/Auth/LoginPage'; +import SignUpPage from './pages/Auth/SignUpPage'; +import GoogleCallbackPage from './pages/Auth/GoogleCallbackPage'; + +// Auth Context +import { useAuth } from './context/AuthContext'; +import { register as apiRegister } from './api/client'; + +// Styles +import './styles/global.css'; + +// Protected Route Component +const ProtectedRoute = ({ children }) => { + const navigate = useNavigate(); + const { isAuthenticated, isLoading } = useAuth(); + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + navigate('/login'); + } + }, [isAuthenticated, isLoading, navigate]); + + if (isLoading) { + return
Loading...
; + } + + return isAuthenticated ? children : null; +}; + +// Dashboard Layout Component (with sidebar) +const DashboardLayout = ({ children, sidebarWidth, isDarkMode, onThemeToggle, onSidebarWidthChange }) => { + return ( + <> + + {React.cloneElement(children, { sidebarWidth, isDarkMode, onThemeToggle })} + + ); +}; + +function App() { + const auth = useAuth(); + + // Dashboard state + const getInitialSidebarWidth = () => { + if (typeof window === "undefined") return 220; + try { + const stored = window.localStorage.getItem("sidebarExpanded"); + if (stored === null) return 220; + return stored === "true" ? 220 : 80; + } catch { + return 220; + } + }; + + const [sidebarWidth, setSidebarWidth] = useState(getInitialSidebarWidth); + const [isDarkMode, setIsDarkMode] = useState(true); + + const location = useLocation(); + const navigate = useNavigate(); + + // Theme management + useEffect(() => { + localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); + const root = document.documentElement; + if (isDarkMode) { + root.classList.remove('light'); + } else { + root.classList.add('light'); + } + }, [isDarkMode]); + + // Authentication handlers + const handleUserLogin = async (email, password, remember = true) => { + await auth.login(email, password, remember); + navigate('/dashboard'); + }; + + const handleUserLogout = () => { + auth.logout(); + navigate('/'); + }; + + const handleSignUp = async (signUpData) => { + const email = signUpData?.email; + const password = signUpData?.password; + + if (!email || !password) { + throw new Error('Email and password are required'); + } + + // Create the user in the DB, then sign them in. + await apiRegister(email, password); + await auth.login(email, password, true); + navigate('/dashboard'); + }; + + const handleThemeToggle = () => { + setIsDarkMode(!isDarkMode); + }; + + const handleSidebarWidthChange = (width) => { + setSidebarWidth(width); + }; + + // Check if current route should show sidebar + const isDashboardRoute = ['/dashboard', '/evidence-scanner', '/styleguide', '/cloud-platforms', '/scans', '/settings', '/account'].includes(location.pathname) || location.pathname.startsWith('/scans/'); + + return ( +
+ + {/* Public Routes */} + navigate('/login')} + /> + } + /> + + navigate('/')} + onSignInClick={() => navigate('/login')} + /> + } + /> + + navigate('/login')} + /> + } + /> + + navigate('/signup')} + /> + } + /> + + } + /> + + navigate('/login')} + /> + } + /> + + {/* Protected Dashboard Routes */} + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + } + /> + + {/* Fallback route */} + navigate('/login')} + onAboutClick={() => navigate('/about')} + onContactClick={() => navigate('/contact')} + /> + } + /> + +
+ ); +} + +export default App; diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 00000000..062c6936 --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,237 @@ +const API_BASE_URL = import.meta.env.VITE_API_URL; + +if (!API_BASE_URL) { + throw new Error('VITE_API_URL environment variable must be set'); +} + +export class APIError extends Error { + constructor(message, status, payload) { + super(message); + this.name = "APIError"; + this.status = status; + this.payload = payload; + } +} + +// Helper for making authenticated requests +async function fetchWithAuth(endpoint, token, options = {}) { + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + try { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + ...options, + headers, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new APIError(error.detail || 'Request failed', response.status, error); + } + + return response.json(); + } catch (error) { + if (error instanceof APIError) { + throw error; + } + throw new APIError(error?.message || 'Network error', 0); + } +} + +// Auth endpoints +export async function login(email, password) { + try { + const response = await fetch(`${API_BASE_URL}/v1/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + username: email, + password: password, + }), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: 'Login failed' })); + throw new APIError(error.detail || 'Invalid credentials', response.status, error); + } + + return response.json(); + } catch (error) { + throw error; + } +} + +export async function register(email, password) { + return fetchWithAuth('/v1/auth/register', null, { + method: 'POST', + body: JSON.stringify({ email, password }), + }); +} + +export async function logout(token) { + // Backend uses FastAPI Users; JWT logout typically returns 204 No Content. + // This is best-effort because JWTs are stateless; the client must clear local auth. + if (!token) return; + + const response = await fetch(`${API_BASE_URL}/v1/auth/logout`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new APIError(error.detail || 'Logout failed', response.status, error); + } + + // 204 No Content (common for logout); nothing to parse. + if (response.status === 204) return; + + // If the backend ever returns JSON, tolerate empty bodies. + return response.json().catch(() => null); +} + +export async function getCurrentUser(token) { + return fetchWithAuth('/v1/auth/users/me', token); +} + +// Platform endpoints +export async function getPlatforms(token) { + return fetchWithAuth('/v1/platforms', token); +} + +// M365 Connection endpoints +export async function getConnections(token) { + return fetchWithAuth('/v1/m365-connections/', token); +} + +export async function createConnection(token, data) { + return fetchWithAuth('/v1/m365-connections/', token, { + method: 'POST', + body: JSON.stringify(data), + }); +} + +export async function updateConnection(token, id, data) { + return fetchWithAuth(`/v1/m365-connections/${id}`, token, { + method: 'PUT', + body: JSON.stringify(data), + }); +} + +export async function deleteConnection(token, id) { + const response = await fetch(`${API_BASE_URL}/v1/m365-connections/${id}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || 'Failed to delete connection'); + } + + // DELETE returns 204 No Content, so don't try to parse JSON + return; +} + +export async function testConnection(token, id) { + return fetchWithAuth(`/v1/m365-connections/${id}/test`, token, { + method: 'POST', + }); +} + +// Benchmark endpoints +export async function getBenchmarks(token) { + return fetchWithAuth('/v1/benchmarks', token); +} + +// Scan endpoints +export async function getScans(token) { + return fetchWithAuth('/v1/scans/', token); +} + +export async function getScan(token, id) { + return fetchWithAuth(`/v1/scans/${id}`, token); +} + +export async function createScan(token, data) { + return fetchWithAuth('/v1/scans/', token, { + method: 'POST', + body: JSON.stringify(data), + }); +} + +// Evidence scanner endpoints +export async function getEvidenceStrategies() { + // Frontend -> Backend + // GET /v1/evidence/strategies + // + // Returns an array of strategy objects, e.g. + // [{ name, description, category, severity, evidence_types }, ...] + // (see backend-api/app/api/v1/evidence.py -> strategies()). + return fetchWithAuth('/v1/evidence/strategies', null); +} + +export async function scanEvidence(token, { strategyName, file }) { + // Frontend -> Backend + // POST /v1/evidence/scan (multipart/form-data) + // + // This uploads an evidence file and tells the backend which strategy to run. + // The user is derived from the Bearer token (server-side), not a client-provided user_id. + // Backend returns a JSON payload that the UI renders in the Results section. + if (!strategyName) { + throw new Error('Strategy is required'); + } + if (!file) { + throw new Error('Evidence file is required'); + } + + const formData = new FormData(); + // These field names must match the FastAPI endpoint signature in: + // backend-api/app/api/v1/evidence.py -> scan(...) + formData.append('strategy_name', strategyName); + formData.append('evidence', file); + + const headers = {}; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const response = await fetch(`${API_BASE_URL}/v1/evidence/scan`, { + method: 'POST', + headers, + body: formData, + }); + + if (!response.ok) { + // The backend may respond with JSON (FastAPI error) or plain text. + // We parse best-effort and throw APIError so callers can display a message. + const raw = await response.text().catch(() => ''); + try { + const error = raw ? JSON.parse(raw) : { detail: response.statusText }; + throw new APIError(error.detail || 'Scan failed', response.status, error); + } catch { + throw new APIError(raw || 'Scan failed', response.status); + } + } + + return response.json(); +} + +export function getEvidenceReportUrl(filename) { + // Frontend helper to build a direct download URL for a generated report. + // Backend endpoint: GET /v1/evidence/reports/{filename} + if (!filename) return ''; + return `${API_BASE_URL}/v1/evidence/reports/${encodeURIComponent(filename)}`; +} diff --git a/frontend/src/components/ComplianceChart.js b/frontend/src/components/ComplianceChart.jsx similarity index 100% rename from frontend/src/components/ComplianceChart.js rename to frontend/src/components/ComplianceChart.jsx diff --git a/frontend/src/components/Dropdown.css b/frontend/src/components/Dropdown.css index 77cea233..05f1ac74 100644 --- a/frontend/src/components/Dropdown.css +++ b/frontend/src/components/Dropdown.css @@ -2,22 +2,23 @@ NB 14 September 2025 - Currently in use for chart type selection but this has been made general-purpose and can be used for anything. Updated with theme support while preserving original design */ -/* Dark theme (default) - preserving original colors */ +/* Dark theme (default) - aligned with Landing/Dashboard palette */ .chart-dropdown { - --dropdown-bg-primary: #1e293b; - --dropdown-bg-secondary: #334155; - --dropdown-text-primary: #94a3b8; - --dropdown-text-secondary: #64748b; - --dropdown-text-hover: #e2e8f0; - --dropdown-border-color: #475569; - --dropdown-border-hover: #64748b; - --dropdown-border-focus: #94a3b8; - --dropdown-selected-bg: rgba(148, 163, 184, 0.15); - --dropdown-selected-hover: rgba(148, 163, 184, 0.2); - --dropdown-hover-bg: rgba(148, 163, 184, 0.1); - --dropdown-option-hover: #334155; - --dropdown-shadow: rgba(0, 0, 0, 0.3); - --dropdown-focus-shadow: rgba(148, 163, 184, 0.2); + /* Use a truly opaque surface for the open menu so options remain readable above any content. */ + --dropdown-bg-primary: rgb(var(--surface-2, 30 41 59)); + --dropdown-bg-secondary: var(--bg-tertiary, rgba(255, 255, 255, 0.05)); + --dropdown-text-primary: var(--text-secondary, #b0c4de); + --dropdown-text-secondary: var(--text-tertiary, #94a3b8); + --dropdown-text-hover: var(--text-primary, #ffffff); + --dropdown-border-color: var(--border-color, rgba(59, 130, 246, 0.12)); + --dropdown-border-hover: rgba(59, 130, 246, 0.35); + --dropdown-border-focus: rgba(59, 130, 246, 0.6); + --dropdown-selected-bg: rgba(59, 130, 246, 0.12); + --dropdown-selected-hover: rgba(59, 130, 246, 0.18); + --dropdown-hover-bg: rgba(255, 255, 255, 0.04); + --dropdown-option-hover: var(--dropdown-bg-secondary); + --dropdown-shadow: rgba(0, 0, 0, 0.35); + --dropdown-focus-shadow: rgba(59, 130, 246, 0.25); } /* Light theme overrides */ @@ -29,13 +30,13 @@ Updated with theme support while preserving original design */ --dropdown-text-hover: #0f172a; --dropdown-border-color: #e2e8f0; --dropdown-border-hover: #cbd5e1; - --dropdown-border-focus: #64748b; - --dropdown-selected-bg: rgba(34, 211, 238, 0.15); - --dropdown-selected-hover: rgba(34, 211, 238, 0.25); + --dropdown-border-focus: #3b82f6; + --dropdown-selected-bg: rgba(59, 130, 246, 0.12); + --dropdown-selected-hover: rgba(59, 130, 246, 0.18); --dropdown-hover-bg: rgba(100, 116, 139, 0.1); --dropdown-option-hover: #f8fafc; --dropdown-shadow: rgba(0, 0, 0, 0.15); - --dropdown-focus-shadow: rgba(100, 116, 139, 0.2); + --dropdown-focus-shadow: rgba(59, 130, 246, 0.2); } .chart-dropdown { diff --git a/frontend/src/components/Dropdown.js b/frontend/src/components/Dropdown.jsx similarity index 100% rename from frontend/src/components/Dropdown.js rename to frontend/src/components/Dropdown.jsx diff --git a/frontend/src/components/Sidebar.css b/frontend/src/components/Sidebar.css index 5c0d558a..5db25f45 100644 --- a/frontend/src/components/Sidebar.css +++ b/frontend/src/components/Sidebar.css @@ -15,12 +15,12 @@ outline: 1px solid #3F3F3F; /* Dark theme (default) */ - background-color: #1E293B; + background-color: #0a1628; } /* Dark theme explicit */ .sidebar.dark { - background-color: #1E293B; + background-color: #0a1628; box-shadow: 4px 0 10px rgba(0, 0, 0, 0.2); outline: 1px solid #3F3F3F; } @@ -99,7 +99,7 @@ flex-direction: column; align-items: center; margin-top: auto; - border-top: 1px solid #334155; + border-top: 1px solid rgba(59, 130, 246, 0.12); padding-top: 30px; gap: 15px; transition: border-color 0.3s ease; @@ -156,9 +156,9 @@ /* Styling for currently selected nav item or setting */ .nav-link.active { - background-color: #22D3EE; - color: #0F172A; - box-shadow: 0 4px 20px rgba(34, 211, 238, 0.4); + background-color: #3b82f6; + color: #ffffff; + box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); } /* Hover effects on navigation items - Dark theme */ @@ -176,7 +176,7 @@ /* Hover effect when the navigation item is also the currently selected item */ .nav-link.active:hover { - background-color: #0891B2; + background-color: #2563eb; color: white; } diff --git a/frontend/src/components/Sidebar.js b/frontend/src/components/Sidebar.js deleted file mode 100644 index 881f2f16..00000000 --- a/frontend/src/components/Sidebar.js +++ /dev/null @@ -1,149 +0,0 @@ -//This component establishes a vertical navigation section along the left side of the screen which will be able to be toggled between condensed and expanded sizes -//Last updated 17 September 2025 -//Added theme support - -import React, { useState } from "react"; -import './Sidebar.css'; - - -//Button component that we use throughout the sidebar -//Parameters: -//href - link reference -//name - text to display in expanded view -//icon - text to display in collapsed view - -const NavButton = ({ href, name, icon, isExpanded, isActive = false, onClick }) => { - const handleClick = (clickEvent) => { - if (onClick) { - clickEvent.preventDefault(); - onClick(clickEvent); - } - }; - - return ( -
  • - - {icon} - {isExpanded && {name}} - -
  • - ); -}; - -// Main sidebar component -const Sidebar = ({ onWidthChange, isDarkMode = true }) => { - const [isExpanded, setIsExpanded] = useState(true); //Track whether sidebar is expanded - const [activeItem, setActiveItem] = useState('home'); // Track active navigation item - const [searchValue, setSearchValue] = useState(''); // Track search input value - - // Add this line right after the component declaration for debugging -console.log('Sidebar isDarkMode:', isDarkMode); - //Event to toggle collapsed state and notify parents that the width has changed - const toggleSidebar = () => { - const newExpanded = !isExpanded; - setIsExpanded(newExpanded); - onWidthChange(newExpanded ? 220 : 80); - }; - - //Set active item to the key of whichever nav button was clicked - const handleNavClick = (itemKey) => { - setActiveItem(itemKey); - }; - - //Once search is functional, this search value should be used as the search parameter. Just a placeholder for now, though. - const handleSearchChange = (typed) => { - setSearchValue(typed.target.value); - }; - - return ( - - ); -}; - -export default Sidebar; \ No newline at end of file diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx new file mode 100644 index 00000000..4cf80258 --- /dev/null +++ b/frontend/src/components/Sidebar.jsx @@ -0,0 +1,204 @@ +//This component establishes a vertical navigation section along the left side of the screen which will be able to be toggled between condensed and expanded sizes +//Last updated 17 September 2025 +//Added theme support + +import React, { useState } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { + LayoutDashboard, + Cloud, + FileSearch, + ShieldCheck, + FileText, + Settings, + User, + Menu, + ArrowLeft, + Search, +} from "lucide-react"; +import "./Sidebar.css"; + + +//Button component that we use throughout the sidebar +//Parameters: +//href - link reference +//name - text to display in expanded view +//icon - text to display in collapsed view + +const NavButton = ({ href, name, icon: Icon, isExpanded, isActive = false, onClick }) => { + const handleClick = (clickEvent) => { + if (onClick) { + clickEvent.preventDefault(); + onClick(clickEvent); + } + }; + + return ( +
  • + + + {isExpanded && {name}} + +
  • + ); +}; + +// Main sidebar component +const SIDEBAR_EXPANDED_KEY = "sidebarExpanded"; + +const Sidebar = ({ onWidthChange = () => {}, isDarkMode = true }) => { + const navigate = useNavigate(); + const location = useLocation(); + const [isExpanded, setIsExpanded] = useState(() => { + if (typeof window === "undefined") return true; + try { + const stored = window.localStorage.getItem(SIDEBAR_EXPANDED_KEY); + if (stored === null) return true; + return stored === "true"; + } catch { + return true; + } + }); // Track whether sidebar is expanded (persisted) + const [searchValue, setSearchValue] = useState(''); // Track search input value + + // Determine active item based on current route + const getActiveItem = () => { + const path = location.pathname; + if (path === '/dashboard') return 'home'; + if (path === '/cloud-platforms') return 'cloud-platforms'; + if (path.startsWith('/scans')) return 'scans'; + if (path === '/evidence-scanner') return 'tasks'; + if (path === '/reports') return 'reports'; + if (path === '/settings') return 'settings'; + if (path === '/account') return 'account'; + return 'home'; + }; + + const activeItem = getActiveItem(); + + //Event to toggle collapsed state and notify parents that the width has changed + const toggleSidebar = () => { + const newExpanded = !isExpanded; + setIsExpanded(newExpanded); + onWidthChange(newExpanded ? 220 : 80); + if (typeof window !== "undefined") { + try { + window.localStorage.setItem(SIDEBAR_EXPANDED_KEY, String(newExpanded)); + } catch { + // ignore storage errors (private mode, blocked, etc.) + } + } + }; + + //Navigate to the specified route + const handleNavClick = (itemKey, route) => { + navigate(route); + }; + + //Once search is functional, this search value should be used as the search parameter. Just a placeholder for now, though. + const handleSearchChange = (typed) => { + setSearchValue(typed.target.value); + }; + + return ( + + ); +}; + +export default Sidebar; diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx new file mode 100644 index 00000000..e42766f9 --- /dev/null +++ b/frontend/src/context/AuthContext.jsx @@ -0,0 +1,157 @@ +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; +import { login as apiLogin, getCurrentUser, APIError } from "../api/client"; + +const AuthContext = createContext(null); + +const TOKEN_KEY = "token"; +const USER_KEY = "user"; + +function safeJsonParse(value) { + if (!value) return null; + try { + return JSON.parse(value); + } catch { + return null; + } +} + +function getStoredToken() { + if (typeof window === "undefined") return null; + return window.localStorage.getItem(TOKEN_KEY) || window.sessionStorage.getItem(TOKEN_KEY); +} + +function getStoredUser() { + if (typeof window === "undefined") return null; + return ( + safeJsonParse(window.localStorage.getItem(USER_KEY)) || + safeJsonParse(window.sessionStorage.getItem(USER_KEY)) + ); +} + +function clearStoredAuth() { + if (typeof window === "undefined") return; + window.localStorage.removeItem(TOKEN_KEY); + window.localStorage.removeItem(USER_KEY); + window.sessionStorage.removeItem(TOKEN_KEY); + window.sessionStorage.removeItem(USER_KEY); +} + +function persistAuth(accessToken, userData, remember) { + if (typeof window === "undefined") return; + const storage = remember ? window.localStorage : window.sessionStorage; + const other = remember ? window.sessionStorage : window.localStorage; + + storage.setItem(TOKEN_KEY, accessToken); + storage.setItem(USER_KEY, JSON.stringify(userData)); + + // Ensure we don't have stale auth state in the other storage + other.removeItem(TOKEN_KEY); + other.removeItem(USER_KEY); +} + +export function AuthProvider({ children }) { + const [user, setUser] = useState(() => getStoredUser()); + const [token, setToken] = useState(() => getStoredToken()); + const [isLoading, setIsLoading] = useState(true); + const skipNextValidationRef = useRef(false); + + const isAuthenticated = !!token && !!user; + + // Validate token on mount + useEffect(() => { + async function validateToken() { + if (!token) { + setIsLoading(false); + return; + } + + // Avoid a duplicate /users/me call right after a successful login. + if (skipNextValidationRef.current) { + skipNextValidationRef.current = false; + setIsLoading(false); + return; + } + + try { + const userData = await getCurrentUser(token); + setUser(userData); + + // Refresh cached user in whichever storage currently holds the token + const inLocal = typeof window !== "undefined" && window.localStorage.getItem(TOKEN_KEY) === token; + const storage = typeof window !== "undefined" && inLocal ? window.localStorage : window.sessionStorage; + if (typeof window !== "undefined") { + storage.setItem(USER_KEY, JSON.stringify(userData)); + } + } catch (error) { + // Only clear auth when the backend confirms token is invalid/expired + if (error instanceof APIError && (error.status === 401 )) { + clearStoredAuth(); + setToken(null); + setUser(null); + } + } finally { + setIsLoading(false); + } + } + + validateToken(); + }, [token]); + + async function login(email, password, remember = true) { + const response = await apiLogin(email, password); + const accessToken = response.access_token; + + // Fetch user data + const userData = await getCurrentUser(accessToken); + persistAuth(accessToken, userData, remember); + + skipNextValidationRef.current = true; + setToken(accessToken); + setUser(userData); + return userData; + } + + async function loginWithAccessToken(accessToken, remember = false) { + if (!accessToken) { + throw new Error("Access token is required"); + } + + const userData = await getCurrentUser(accessToken); + persistAuth(accessToken, userData, remember); + + skipNextValidationRef.current = true; + setToken(accessToken); + setUser(userData); + return userData; + } + + function logout() { + clearStoredAuth(); + setToken(null); + setUser(null); + } + + const value = { + user, + token, + isAuthenticated, + isLoading, + login, + loginWithAccessToken, + logout, + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/frontend/src/index.js b/frontend/src/main.jsx similarity index 75% rename from frontend/src/index.js rename to frontend/src/main.jsx index a6cd61b3..02949254 100644 --- a/frontend/src/index.js +++ b/frontend/src/main.jsx @@ -1,6 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter as Router } from 'react-router-dom'; +import { AuthProvider } from './context/AuthContext'; + // Tailwind + tokens + base (includes tokens.css & components.css) import './styles/global.css'; @@ -8,13 +10,15 @@ import './styles/global.css'; // Legacy app styles (keeps current look intact) import './index.css'; -import App from './App'; +import App from './App.jsx'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + + + ); \ No newline at end of file diff --git a/frontend/src/pages/AccountPage.css b/frontend/src/pages/AccountPage.css new file mode 100644 index 00000000..9bf4a490 --- /dev/null +++ b/frontend/src/pages/AccountPage.css @@ -0,0 +1,105 @@ +/* Account Page (dashboard protected) */ +.account-page { + min-height: 100vh; + background: var(--bg-primary); + padding: 24px; + transition: background-color 0.3s ease; +} + +.account-page.light { + --bg-primary: #f8fafc; + --bg-secondary: #ffffff; + --bg-tertiary: #e2e8f0; + --text-primary: #1e293b; + --text-secondary: #64748b; + --text-tertiary: #94a3b8; + --border-color: #e2e8f0; +} + +.account-container { + max-width: 1000px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 24px; +} + +.page-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.page-header .header-content { + display: flex; + align-items: center; + gap: 16px; + color: var(--text-primary); +} + +.page-header .header-content svg { + color: var(--teal, #64dfdf); +} + +.page-header .header-text h1 { + font-size: 24px; + font-weight: bold; + color: var(--text-primary); + margin: 0; +} + +.page-header .header-text p { + color: var(--text-secondary); + font-size: 14px; + margin: 0; +} + +.account-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 24px; +} + +.account-card h3 { + color: var(--text-primary); + font-size: 18px; + font-weight: 600; + margin: 0 0 14px 0; +} + +.account-meta { + display: flex; + flex-wrap: wrap; + gap: 24px; + padding-top: 10px; + border-top: 1px solid var(--border-color); +} + +.meta-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.meta-label { + color: var(--text-tertiary); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.meta-value { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; +} + +.account-note { + color: var(--text-secondary); + font-size: 14px; + margin: 14px 0 0 0; + line-height: 1.5; +} + diff --git a/frontend/src/pages/AccountPage.jsx b/frontend/src/pages/AccountPage.jsx new file mode 100644 index 00000000..6a9e0818 --- /dev/null +++ b/frontend/src/pages/AccountPage.jsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Loader2, LogOut, User } from "lucide-react"; +import { logout as apiLogout } from "../api/client"; +import { useAuth } from "../context/AuthContext"; +import "./AccountPage.css"; + +export default function AccountPage({ sidebarWidth = 220, isDarkMode = true }) { + const navigate = useNavigate(); + const { user, token, logout: clearAuth } = useAuth(); + const [isLoggingOut, setIsLoggingOut] = useState(false); + + const primaryLabel = + user?.email || user?.username || user?.name || user?.id || "Signed in"; + + const handleLogout = async () => { + if (isLoggingOut) return; + setIsLoggingOut(true); + + try { + await apiLogout(token); + } catch (error) { + // Best-effort: even if API logout fails (network, already-expired token), + // we still clear local auth so the user is signed out client-side. + console.warn("Logout request failed; clearing local auth anyway:", error); + } finally { + clearAuth(); + navigate("/"); + } + }; + + return ( +
    +
    +
    +
    + +
    +

    Account

    +

    Profile and user preferences.

    +
    +
    + +
    + +
    +

    Profile

    +
    +
    + User + {primaryLabel} +
    +
    +
    +
    +
    + ); +} + diff --git a/frontend/src/pages/Auth/GoogleCallbackPage.jsx b/frontend/src/pages/Auth/GoogleCallbackPage.jsx new file mode 100644 index 00000000..550b8481 --- /dev/null +++ b/frontend/src/pages/Auth/GoogleCallbackPage.jsx @@ -0,0 +1,213 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { AlertCircle, Loader2 } from "lucide-react"; + +import "./LoginPage.css"; +import "../Landing/LandingPage.css"; + +import LoginHeader from "./components/LoginHeader"; +import BrandPanel from "./components/BrandPanel"; +import LandingFooter from "../Landing/components/LandingFooter"; +import { useAuth } from "../../context/AuthContext"; + +const CALLBACK_CACHE_KEY = "autoaudit.oauth.google.callback.params"; + +function safeJsonParse(value) { + if (!value) return null; + try { + return JSON.parse(value); + } catch { + return null; + } +} + +function readCachedCallbackParams() { + if (typeof window === "undefined") return null; + try { + return safeJsonParse(window.sessionStorage.getItem(CALLBACK_CACHE_KEY)); + } catch { + return null; + } +} + +function writeCachedCallbackParams(payload) { + if (typeof window === "undefined") return; + try { + window.sessionStorage.setItem(CALLBACK_CACHE_KEY, JSON.stringify(payload)); + } catch { + // best-effort + } +} + +function clearCachedCallbackParams() { + if (typeof window === "undefined") return; + try { + window.sessionStorage.removeItem(CALLBACK_CACHE_KEY); + } catch { + // best-effort + } +} + +function getOAuthParams() { + const rawHash = typeof window !== "undefined" ? window.location.hash : ""; + const hash = rawHash.startsWith("#") ? rawHash.slice(1) : rawHash; + const merged = new URLSearchParams(hash); + + // Some environments/providers return parameters in the query string. + const rawSearch = typeof window !== "undefined" ? window.location.search : ""; + const search = rawSearch.startsWith("?") ? rawSearch.slice(1) : rawSearch; + const searchParams = new URLSearchParams(search); + for (const [key, value] of searchParams.entries()) { + if (!merged.has(key)) merged.set(key, value); + } + + return merged; +} + +const GoogleCallbackPage = () => { + const navigate = useNavigate(); + const auth = useAuth(); + + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + + async function finish() { + const params = getOAuthParams(); + + // Prefer params from the URL, but fall back to a cached copy. + // React 18 StrictMode intentionally mounts effects twice in development; the first + // run clears the hash, so the second run would otherwise see no token. + const urlPayload = { + access_token: + params.get("access_token") || params.get("token") || params.get("accessToken"), + token_type: params.get("token_type") || params.get("tokenType"), + error: params.get("error"), + error_description: params.get("error_description") || params.get("errorDescription"), + }; + + if (urlPayload.access_token || urlPayload.error) { + writeCachedCallbackParams(urlPayload); + } + + const cachedPayload = readCachedCallbackParams(); + const accessToken = urlPayload.access_token || cachedPayload?.access_token; + const oauthError = urlPayload.error || cachedPayload?.error; + const oauthErrorDescription = urlPayload.error_description || cachedPayload?.error_description; + + if (oauthError) { + if (!cancelled) { + setError(oauthErrorDescription || oauthError); + } + clearCachedCallbackParams(); + return; + } + + if (!accessToken) { + if (!cancelled) { + setError("Missing access token. Please try signing in again."); + } + clearCachedCallbackParams(); + return; + } + + // Remove the token fragment from the URL as soon as possible. + try { + window.history.replaceState({}, document.title, window.location.pathname); + } catch { + // best-effort + } + + try { + // SSO sessions must use sessionStorage (remember=false). + await auth.loginWithAccessToken(accessToken, false); + clearCachedCallbackParams(); + if (!cancelled) { + // Hard redirect so we always leave the callback page after external OAuth. + window.location.replace("/dashboard"); + } + } catch (err) { + if (!cancelled) { + setError(err?.message || "Google sign-in failed. Please try again."); + } + clearCachedCallbackParams(); + } + } + + finish(); + return () => { + cancelled = true; + }; + // We intentionally run this effect only once on initial load of the callback page. + // AuthContext updates after login would otherwise re-run the effect and the URL + // hash may already be cleared, leading to a false “missing access token” error. + }, []); + + return ( +
    + +
    + +
    +
    + {error ? ( + <> +
    +

    Sign-in failed

    +

    We couldn’t complete Google sign-in. Please try again.

    +
    +
    + + {error} +
    + + + + ) : ( +
    + +
    Please wait while we sign you in.
    +
    + )} +
    +
    +
    + +
    + ); +}; + +export default GoogleCallbackPage; + diff --git a/frontend/src/pages/Auth/LoginPage.css b/frontend/src/pages/Auth/LoginPage.css index f1074a1e..1acc051a 100644 --- a/frontend/src/pages/Auth/LoginPage.css +++ b/frontend/src/pages/Auth/LoginPage.css @@ -1,297 +1,564 @@ -.login-container { - display: flex; +.login-page { + --bg-dark: #0a1628; + --bg-darker: #0f1f38; + --text-main: #ffffff; + --text-muted: #b0c4de; + --accent: #3b82f6; + --accent-strong: #2563eb; + --border: rgba(59, 130, 246, 0.1); + min-height: 100vh; - background: #0f172a; + background: #0a1628; + color: #ffffff; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + display: flex; + flex-direction: column; + overflow-x: hidden; +} + +.auth-header { + padding: 1.5rem 5%; + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(10, 22, 40, 0.95); + border-bottom: 1px solid var(--border); + backdrop-filter: blur(10px); } +.auth-logo img { + height: 70px; + width: auto; + transition: transform 0.3s ease; + display: block; +} -.top-nav { - position: absolute; - top: 20px; - right: 30px; - z-index: 10; +.auth-logo img:hover { + transform: scale(1.04); } +.auth-nav { + display: flex; + gap: 1.5rem; + align-items: center; +} -.top-nav .home-link { - display: inline-block; - background: transparent; - color: #fdffff; - font-weight: 600; - font-size: 16px; +.auth-nav a { + color: #e0e0e0; text-decoration: none; - padding: 12px 16px; - border-radius: 8px; - transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease; - border: 1px solid transparent; + font-weight: 500; + position: relative; } +.auth-nav a::after { + content: ""; + position: absolute; + left: 0; + bottom: -4px; + width: 0; + height: 2px; + background: var(--accent); + transition: width 0.3s ease; +} -.top-nav .home-link:hover, -.top-nav .home-link:focus-visible { - background: #36dad6; - color: #ffffff; - border-color: #36dad6; - transform: translateY(-1px); - text-decoration: none; +.auth-nav a:hover { + color: var(--accent); } +.auth-nav a:hover::after { + width: 100%; +} +.login-main { + flex: 1; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-height: calc(100vh - 120px); + gap: 1.5rem; + padding: 1.5rem 5% 2.5rem; +} -.login-left { - width: 40%; +.login-brand { + background-image: none; + background-clip: unset; + -webkit-background-clip: unset; + color: rgba(255, 255, 255, 1); + background-color: unset; + background: unset; + position: relative; + padding: 2.5rem 3rem; display: flex; align-items: center; justify-content: center; - padding: 40px; + overflow: hidden; + border-radius: 24px; } -.login-content { - width: 100%; - max-width: 400px; +.login-brand::before, +.login-brand::after { + content: ""; + position: absolute; + border-radius: 50%; + opacity: 0.6; + animation: brandPulse 8s ease-in-out infinite; } -.logo-img { - width: 200px; - height: auto; - margin-bottom: 16px; - border-radius: 15px; - margin-left: 5px; - box-shadow: 1px 2px 20px 2px #0ff; +.login-brand::before { + width: 480px; + height: 480px; + background: radial-gradient(circle, rgba(59, 130, 246, 0.15) 0%, transparent 70%); + top: -160px; + right: -160px; } -.logo-section { - text-align: center; - margin-bottom: 20px; +.login-brand::after { + width: 360px; + height: 360px; + background: radial-gradient(circle, rgba(59, 130, 246, 0.15) 0%, transparent 70%); + bottom: -120px; + left: -120px; + animation-delay: 2s; } -.logo-section h1 { - font-size: 32px; - font-weight: bold; - color: white; - margin: 0 0 8px 0; +@keyframes brandPulse { + 0%, + 100% { + transform: scale(1); + opacity: 0.3; + } + 50% { + transform: scale(1.2); + opacity: 0.7; + } } -.subtitle { - color: #36dad6; - font-size: 13px; - margin: 0; +.brand-particle { + position: absolute; + width: 4px; + height: 4px; + background: var(--accent); + border-radius: 50%; + opacity: 0.4; + animation: floatParticle 18s infinite; +} + +.brand-particle-1 { + left: 12%; + top: 25%; +} +.brand-particle-2 { + left: 30%; + top: 70%; + animation-delay: 2s; +} +.brand-particle-3 { + left: 52%; + top: 45%; + animation-delay: 4s; +} +.brand-particle-4 { + left: 68%; + top: 80%; + animation-delay: 1s; +} +.brand-particle-5 { + left: 82%; + top: 35%; + animation-delay: 3s; +} +.brand-particle-6 { + left: 20%; + top: 85%; + animation-delay: 5s; +} + +@keyframes floatParticle { + 0%, + 100% { + transform: translate(0, 0); + opacity: 0.4; + } + 50% { + transform: translate(25px, -60px); + opacity: 0.8; + } +} + +.brand-content { + position: relative; + z-index: 1; + text-align: center; + max-width: 420px; + display: flex; + flex-direction: column; + gap: 1.5rem; + align-items: center; } -.login-form { - background: #1e293b; - border-radius: 16px; - padding: 32px; - border: 1px solid #334155; +/* BrandPanel: keep headings centered, but left-align span text content */ +.login-brand span { + text-align: left; } -.form-header { - text-align: center; - margin-bottom: 24px; +.brand-kicker { + text-transform: uppercase; + letter-spacing: 3px; + color: var(--accent); + font-size: 0.9rem; + margin-bottom: 0.5rem; } -.form-header h2 { - font-size: 20px; - font-weight: 600; - color: white; - margin: 0 0 8px 0; +.brand-logo { + width: 180px; + height: auto; + margin: 0 auto 1rem; + filter: drop-shadow(0 10px 25px rgba(59, 130, 246, 0.3)); +} + +.brand-text h1 { + font-size: clamp(1.8rem, 4vw, 2.3rem); + margin-bottom: 1rem; + background: linear-gradient(135deg, #ffffff, var(--accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-align: center; } -.form-header p { - color: #94a3b8; - font-size: 14px; +.brand-text p { + color: #b0c4de; + line-height: 1.6; margin: 0; + text-align: center; } -.form-fields { +.brand-features { + margin-top: 1rem; display: flex; flex-direction: column; - gap: 24px; + gap: 1rem; + width: 100%; } -.field-group { +.brand-feature { display: flex; - flex-direction: column; + align-items: center; + gap: 1rem; + text-align: left; + padding: 1rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 12px; + border: 1px solid var(--border); +} + +.brand-feature-icon { + width: 40px; + height: 40px; + display: grid; + place-items: center; + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + border-radius: 10px; + font-size: 1.2rem; +} + +.login-form-section { + /* background: #0f1f38; */ + display: flex; + align-items: stretch; + justify-content: center; + /* padding: 2.5rem; */ + border-radius: 24px; } -.field-group label { - color: #cbd5e1; - font-size: 14px; - font-weight: 500; - margin-bottom: 8px; +.login-form-card { + width: 100%; + max-width: 480px; + background: rgba(15, 35, 56, 0.9); + border-radius: 18px; + border: none; + padding: 2.25rem; + box-shadow: 0 30px 60px rgba(5, 9, 20, 0.45); + height: 100%; } -.field-group input { - padding: 12px 16px; - background: #334155; - border: 1px solid #475569; - border-radius: 8px; - color: white; - font-size: 14px; +.login-form-header h2 { + font-size: 2rem; + margin-bottom: 0.5rem; } -.field-group input::placeholder { - color: #94a3b8; +.login-form-header p { + color: #b0c4de; } -.field-group input:focus { - outline: none; - border-color: #36dad6; - box-shadow: 0 0 0 1px #36dad6; +.login-form { + margin-top: 2rem; + display: flex; + flex-direction: column; + gap: 1.25rem; } -.password-field { +.form-group label { + display: block; + margin-bottom: 0.4rem; + color: #b0c4de; + font-weight: 500; +} + +.input-wrapper { position: relative; - width: 100%; } -.password-field input { +.input-wrapper input { width: 100%; - padding-right: 48px; + padding: 1rem 1rem 1rem 3rem; + border-radius: 12px; + border: 2px solid rgba(59, 130, 246, 0.2); + background: rgba(255, 255, 255, 0.05); + color: #ffffff; + font-size: 1rem; +} + +.input-wrapper input::placeholder { + color: #6c7a8d; +} + +.input-wrapper input:focus { + outline: none; + border-color: var(--accent); + background: rgba(255, 255, 255, 0.08); + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.12); +} + +.input-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: var(--accent); } .password-toggle { position: absolute; - right: 16px; + right: 1rem; top: 50%; transform: translateY(-50%); background: none; border: none; - color: #94a3b8; + color: #b0c4de; cursor: pointer; - font-size: 16px; } .password-toggle:hover { - color: #cbd5e1; + color: var(--accent); } .form-options { display: flex; justify-content: space-between; align-items: center; + font-size: 0.9rem; } -.checkbox-label { +.checkbox-wrapper { display: flex; align-items: center; - color: #cbd5e1; - font-size: 14px; + gap: 0.5rem; cursor: pointer; + color: #b0c4de; } -.checkbox-label input { - margin-right: 8px; - width: 16px; - height: 16px; +.checkbox-wrapper input { + width: 18px; + height: 18px; + accent-color: var(--accent); } .forgot-link { - color: #36dad6; - font-size: 14px; + color: var(--accent); text-decoration: none; } -.forgot-link:hover { - color: #36dad6; -} - - -.signin-btn { +.btn-signin { width: 100%; - background: #36dad6; - color: #ffffff; - font-weight: 600; - padding: 12px 16px; + padding: 1rem; + border-radius: 12px; border: none; - border-radius: 8px; - font-size: 16px; + background-color: rgba(255, 255, 255, 1); + background-image: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); + color: #ffffff; + font-size: 1rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; cursor: pointer; - transition: transform 0.15s ease, filter 0.15s ease; + transition: transform 0.2s ease, box-shadow 0.2s ease; } -.signin-btn:hover { - filter: brightness(0.95); - transform: translateY(-1px); +.btn-signin:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4); } -.signin-btn:focus-visible { - outline: 2px solid #ffffff; - outline-offset: 2px; +.divider { + display: flex; + align-items: center; + margin: 2rem 0; + color: #b0c4de; + text-transform: uppercase; + font-size: 0.8rem; + letter-spacing: 1px; } +.divider::before, +.divider::after { + content: ""; + flex: 1; + height: 1px; + background: rgba(59, 130, 246, 0.2); +} -.security-notice { - margin-top: 24px; - padding: 16px; - background: #334155; - border-radius: 8px; - border: 1px solid #475569; +.divider span { + padding: 0 1rem; } -.security-content { - display: flex; - align-items: flex-start; +.social-login { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; } -.security-icon { - font-size: 20px; - margin-right: 12px; - margin-top: 2px; +.social-login.single { + grid-template-columns: 1fr; + justify-items: center; } -.security-content h3 { - font-size: 14px; - font-weight: 500; - color: white; - margin: 0 0 4px 0; +.social-btn { + padding: 0.9rem; + border-radius: 12px; + border: 2px solid rgba(59, 130, 246, 0.2); + background: rgba(255, 255, 255, 0.04); + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease; + width: 100%; + max-width: 280px; } -.security-content p { - font-size: 12px; - color: #94a3b8; - margin: 0; - line-height: 1.4; +.social-btn:focus-visible { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.18); } +.social-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} -.login-right { - width: 70%; - position: relative; - overflow: hidden; +.social-btn:disabled:hover { + transform: none; + border-color: rgba(59, 130, 246, 0.2); } -.login-right img { - width: 100%; - height: 100%; - object-fit: cover; +.social-btn:hover { + transform: translateY(-2px); + border-color: var(--accent); } -.image-overlay { - position: absolute; - inset: 0; - background: linear-gradient(to right, rgba(15, 23, 42, 0.8), rgba(15, 23, 42, 0.4), transparent); +.social-icon { + width: 32px; + height: 32px; + border-radius: 10px; + background: rgba(59, 130, 246, 0.15); + display: grid; + place-items: center; + font-weight: 600; + letter-spacing: 0.5px; } +.social-icon svg { + width: 16px; + height: 16px; +} -.signup-redirect { +.social-icon--google { + background: rgba(255, 255, 255, 0.08); +} + +.signup-text { text-align: center; - margin-top: 20px; - color: #94a3b8; - font-size: 14px; + color: #b0c4de; + font-size: 0.95rem; } -.signup-link { +.signup-text button { background: none; border: none; - color: #36dad6; + color: var(--accent); font-weight: 600; cursor: pointer; - text-decoration: underline; - font-size: 14px; - font-family: inherit; } -.signup-link:hover { - color: #f5f6f6; +.signup-text button:hover { + color: var(--accent); +} + +@media (max-width: 1024px) { + .login-main { + grid-template-columns: 1fr; + } + + .login-brand { + min-height: 60vh; + } + + .brand-features { + display: none; + } } + +@media (max-width: 640px) { + .auth-header { + padding: 1rem 1.5rem; + } + + .auth-nav { + gap: 1rem; + } + + .login-brand { + padding: 2.5rem 1.5rem; + } + + .login-form-section { + padding: 2.5rem 1.5rem; + } + + .social-login { + grid-template-columns: 1fr; + } +} +.error-message { + margin: 8px 0; + padding: 8px 10px; + border: 1px solid rgba(255, 80, 80, 0.3); + background-color: rgba(255, 80, 80, 0.08); + color: #ff6b6b; + border-radius: 6px; + font-size: 13px; +} + +.form-helper { + margin-top: 4px; + color: #9aa3b2; + font-size: 12.5px; + line-height: 1.4; +} \ No newline at end of file diff --git a/frontend/src/pages/Auth/LoginPage.js b/frontend/src/pages/Auth/LoginPage.js deleted file mode 100644 index a9cee6a4..00000000 --- a/frontend/src/pages/Auth/LoginPage.js +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useState } from 'react'; -import './LoginPage.css'; - -export default function LoginPage({ onLogin, onSignUpClick }) { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [rememberMe, setRememberMe] = useState(false); - - const handleSubmit = () => { - onLogin(); - }; - - return ( -
    - - -
    - Login bg -
    -
    - -
    -
    -
    - AutoAudit Logo -

    AutoAudit

    -

    Microsoft 365 Compliance Platform

    -
    - -
    -
    -

    Sign In

    -

    Access your compliance dashboard and security insights

    -
    - -
    -
    - - setEmail(e.target.value)} - placeholder="your.email@company.com" - /> -
    - -
    - -
    - setPassword(e.target.value)} - placeholder="Enter your password" - /> - -
    -
    - -
    - - Forgot password? -
    - - - -
    - Don't have an account? - -
    -
    - -
    -
    - 🛡️ -
    -

    Secure Enterprise Access

    -

    Your connection is encrypted and monitored for compliance with enterprise security standards

    -
    -
    -
    -
    -
    -
    -
    - ); -} \ No newline at end of file diff --git a/frontend/src/pages/Auth/LoginPage.jsx b/frontend/src/pages/Auth/LoginPage.jsx new file mode 100644 index 00000000..7f0d1230 --- /dev/null +++ b/frontend/src/pages/Auth/LoginPage.jsx @@ -0,0 +1,22 @@ +import React from "react"; +import "./LoginPage.css"; +import "../Landing/LandingPage.css"; +import LoginHeader from "./components/LoginHeader"; +import BrandPanel from "./components/BrandPanel"; +import SignInPanel from "./components/SignInPanel"; +import LandingFooter from "../Landing/components/LandingFooter"; + +const LoginPage = ({ onLogin, onSignUpClick }) => { + return ( +
    + +
    + + +
    + +
    + ); +}; + +export default LoginPage; diff --git a/frontend/src/pages/Auth/SignUpPage.css b/frontend/src/pages/Auth/SignUpPage.css index bb5af87d..f9c6919b 100644 --- a/frontend/src/pages/Auth/SignUpPage.css +++ b/frontend/src/pages/Auth/SignUpPage.css @@ -1,334 +1,214 @@ -.signup-container { - display: flex; - min-height: 100vh; - background: #0f172a; +.signup-page .login-main { + padding-top: 1.5rem; } -.signup-left { - width: 40%; - display: flex; - align-items: center; - justify-content: center; - padding: 40px; - overflow-y: auto; -} - -.signup-content { - width: 100%; - max-width: 420px; +.signup-brand__content { + max-width: 460px; } -.logo-img { - width: 250px; - height: auto; - margin-bottom: 16px; - border-radius: 15px; - margin-left: 70px; - box-shadow: 1px 2px 20px 2px #36dad6; +.signup-brand__logo { + width: 220px; + filter: drop-shadow(0 12px 35px rgba(59, 130, 246, 0.3)); } -.logo-section { - text-align: center; - margin-bottom: 20px; +.signup-brand__text h1 { + font-size: 2.7rem; + margin-bottom: 0.75rem; + line-height: 1.2; + background: linear-gradient(135deg, #ffffff, var(--accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } -.logo-section h1 { - font-size: 32px; - font-weight: bold; - color: white; - margin: 0 0 8px 0; -} - -.subtitle { - color: #36dad6; - font-size: 13px; - margin: 0; -} - -.top-nav { - position: absolute; - top: 20px; - right: 30px; - z-index: 10; +.signup-brand__text p { + color: #b0c4de; + line-height: 1.6; + margin: 0 0 2rem; } - -.top-nav .home-link { - display: inline-block; - background: transparent; - color: #fdffff; - font-weight: 600; - font-size: 16px; - text-decoration: none; - padding: 12px 16px; - border-radius: 8px; - transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease; - border: 1px solid transparent; -} - - -.top-nav .home-link:hover, -.top-nav .home-link:focus-visible { - background: #36dad6; - color: #ffffff; - border-color: #36dad6; - transform: translateY(-1px); - text-decoration: none; +.signup-brand__features { + display: grid; + gap: 1rem; + width: 100%; } -.signup-form { - background: #1e293b; +.signup-brand__feature { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; border-radius: 16px; - padding: 32px; - border: 1px solid #334155; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.04); + text-align: left; + transition: transform 0.2s ease, border-color 0.2s ease; } -.form-header { - text-align: center; - margin-bottom: 28px; +.signup-brand__feature:hover { + transform: translateX(6px); + border-color: var(--accent); } -.form-header h2 { - font-size: 20px; - font-weight: 600; - color: white; - margin: 0 0 8px 0; +.signup-brand__feature-icon { + width: 42px; + height: 42px; + border-radius: 12px; + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; } -.form-header p { - color: #94a3b8; - font-size: 14px; - margin: 0; +.signup-form-section { + padding-top: 0; } -.form-fields { - display: flex; - flex-direction: column; - gap: 20px; +.signup-form-card { + max-width: 480px; } -.name-row { - display: flex; - flex-direction: column; - gap: 20px; +.signup-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; } -.field-group { +.signup-field { display: flex; flex-direction: column; + gap: 0.5rem; + color: #b0c4de; + font-size: 0.95rem; } -.field-group label { - color: #cbd5e1; - font-size: 14px; - font-weight: 500; - margin-bottom: 8px; -} - -.field-group input { - padding: 12px 16px; - background: #334155; - border: 1px solid #475569; - border-radius: 8px; - color: white; - font-size: 14px; -} - -.field-group input::placeholder { - color: #94a3b8; -} - -.field-group input:focus { - outline: none; - border-color: #36dad6; - box-shadow: 0 0 0 1px #36dad6; -} - -.password-field { +.signup-field .input-wrapper { position: relative; - width: 100%; -} - -.password-field input { - width: 100%; - padding-right: 48px; } -.password-toggle { +.signup-field .input-icon { position: absolute; - right: 16px; + left: 1rem; top: 50%; transform: translateY(-50%); - background: none; - border: none; - color: #94a3b8; - cursor: pointer; - font-size: 16px; + color: var(--accent); } -.password-toggle:hover { - color: #cbd5e1; +.signup-field input { + width: 100%; + padding: 0.9rem 1rem 0.9rem 3rem; + border-radius: 12px; + border: 2px solid rgba(59, 130, 246, 0.2); + background: rgba(255, 255, 255, 0.05); + color: #fff; + font-size: 1rem; } -.form-options { - display: flex; - align-items: flex-start; - margin-top: 4px; +.signup-field input::placeholder { + color: #6c7a8d; } -.checkbox-label { - display: flex; +.signup-field input:focus { + outline: none; + border-color: var(--accent); + background: rgba(255, 255, 255, 0.08); + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.12); +} + +.signup-checkbox { align-items: flex-start; - color: #cbd5e1; - font-size: 13px; - cursor: pointer; - line-height: 1.4; + gap: 0.75rem; + margin-top: 0.5rem; } -.checkbox-label input { - margin-right: 8px; - margin-top: 2px; - width: 16px; - height: 16px; - flex-shrink: 0; +.signup-checkbox input { + margin-top: 0.35rem; + accent-color: var(--accent); } -.terms-link { - color: #36dad6; +.signup-checkbox a { + color: var(--accent); text-decoration: none; - } -.terms-link:hover { - color: #1fc9c0; - text-decoration: underline; +.signup-checkbox a:hover { + color: var(--accent-strong); } -.signup-btn { - width: 100%; - background: #36dad6; - color: black; - font-weight: 600; - padding: 14px 16px; - border: none; - border-radius: 8px; - font-size: 16px; - cursor: pointer; - transition: background-color 0.2s; - margin-top: 8px; +.signup-error { + color: #ff8484; + font-size: 0.9rem; + margin: -0.25rem 0 0.75rem 0; } -.signup-btn:hover { - background: #28c5c0; +.signup-submit { + gap: 0.5rem; } -.login-redirect { - text-align: center; - margin-top: 20px; - color: #94a3b8; - font-size: 14px; +.social-login { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; } -.login-link { - background: none; - border: none; - color: #36dad6; - font-weight: 600; - cursor: pointer; - text-decoration: underline; -} -.login-link:hover { - color: #1fc9c0; +.signup-error { + margin-top: 12px; + padding: 10px 12px; + border-radius: 10px; + background: rgba(239, 68, 68, 0.12); + border: 1px solid rgba(239, 68, 68, 0.28); + color: #ef4444; + font-size: 13px; + line-height: 1.35; } -.security-notice { - margin-top: 24px; - padding: 16px; - background: #334155; - border-radius: 8px; - border: 1px solid #475569; +.signup-btn:hover { + background: var(--accent-strong); } -.security-content { +.social-login .social-btn { display: flex; - align-items: flex-start; -} - -.security-icon { - font-size: 20px; - margin-right: 12px; - margin-top: 2px; -} - -.security-content h3 { - font-size: 14px; - font-weight: 500; - color: white; - margin: 0 0 4px 0; -} + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.85rem; -.security-content p { - font-size: 12px; - color: #94a3b8; - margin: 0; - line-height: 1.4; } -.signup-right { - width: 70%; - position: relative; - overflow: hidden; +.social-login .social-btn svg { + width: 16px; + height: 16px; } -.signup-right img { - width: 100%; - height: 100%; - object-fit: cover; +.signup-text button { + background: none; + border: none; + color: var(--accent); + font-weight: 600; + cursor: pointer; } -.image-overlay { - position: absolute; - inset: 0; - background: linear-gradient(to right, rgba(15, 23, 42, 0.8), rgba(15, 23, 42, 0.4), transparent); +.signup-text button:hover { + color: var(--accent-strong); } @media (max-width: 1024px) { - .signup-container { - flex-direction: column; - } - - .signup-left { - width: 100%; - min-height: 100vh; - padding: 20px; - } - - .signup-right { + .signup-brand__features { display: none; } - - .signup-content { - max-width: 500px; - } -} -@media (max-width: 640px) { - .name-row { + .signup-form-grid { grid-template-columns: 1fr; - gap: 20px; - } - - .signup-form { - padding: 24px 20px; } - - .logo-img { - width: 150px; + + .social-login { + grid-template-columns: 1fr; } - - .top-nav { - top: 15px; - right: 20px; +} + +@media (max-width: 640px) { + .signup-brand__text h1 { + font-size: 2.2rem; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/Auth/SignUpPage.js b/frontend/src/pages/Auth/SignUpPage.js deleted file mode 100644 index 9833d887..00000000 --- a/frontend/src/pages/Auth/SignUpPage.js +++ /dev/null @@ -1,189 +0,0 @@ -import React, { useState } from 'react'; -import './SignUpPage.css'; - -export default function SignUpPage({ onSignUp, onBackToLogin }) { - const [formData, setFormData] = useState({ - firstName: '', - lastName: '', - email: '', - organizationName: '', - password: '', - confirmPassword: '' - }); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const [agreeTerms, setAgreeTerms] = useState(false); - - const handleInputChange = (e) => { - const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: value - })); - }; - - const handleSubmit = () => { - if (formData.password !== formData.confirmPassword) { - alert('Passwords do not match'); - return; - } - if (!agreeTerms) { - alert('Please agree to the terms and conditions'); - return; - } - onSignUp(formData); - }; - - return ( -
    - -
    - Signup bg -
    -
    - -
    -
    -
    - AutoAudit Logo -

    AutoAudit

    -

    Microsoft 365 Compliance Platform

    -
    - -
    -
    -

    Create Account

    -

    Start your compliance journey with AutoAudit

    -
    - -
    -
    -
    - - -
    - -
    - - -
    -
    - -
    - - -
    - -
    - - -
    - -
    - -
    - - -
    -
    - -
    - -
    - - -
    -
    - -
    - -
    - - - -
    - Already have an account? - -
    -
    - -
    -
    - 🛡️ -
    -

    Enterprise Security Standards

    -

    Your data is protected with enterprise-grade encryption and follows strict compliance protocols

    -
    -
    -
    -
    -
    -
    -
    - ); -} \ No newline at end of file diff --git a/frontend/src/pages/Auth/SignUpPage.jsx b/frontend/src/pages/Auth/SignUpPage.jsx new file mode 100644 index 00000000..c073454c --- /dev/null +++ b/frontend/src/pages/Auth/SignUpPage.jsx @@ -0,0 +1,67 @@ +import React, { useState } from "react"; +import "./LoginPage.css"; +import "./SignUpPage.css"; +import LoginHeader from "./components/LoginHeader"; +import LandingFooter from "../Landing/components/LandingFooter"; +import SignupBrandPanel from "./components/SignupBrandPanel"; +import SignupFormPanel from "./components/SignupFormPanel"; + +const emptyForm = { + firstName: "", + lastName: "", + email: "", + organizationName: "", + password: "", + confirmPassword: "", +}; + +export default function SignUpPage({ onSignUp, onBackToLogin }) { + const [formData, setFormData] = useState(emptyForm); + const [submitError, setSubmitError] = useState(""); + + const handleFormChange = (field, value) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + if (submitError) { + setSubmitError(""); + } + }; + + const getSubmitErrorMessage = (error) => { + const message = error?.message || "Sign up failed. Please try again."; + if (message === "REGISTER_USER_ALREADY_EXISTS") { + return "An account with this email already exists."; + } + return message; + }; + + const handleFormSubmit = async (payload) => { + if (!onSignUp) return; + setSubmitError(""); + try { + await onSignUp(payload); + setFormData(emptyForm); + } catch (error) { + setSubmitError(getSubmitErrorMessage(error)); + } + }; + + return ( +
    + +
    + + +
    + +
    + ); +} diff --git a/frontend/src/pages/Auth/components/BrandPanel.jsx b/frontend/src/pages/Auth/components/BrandPanel.jsx new file mode 100644 index 00000000..8db0f421 --- /dev/null +++ b/frontend/src/pages/Auth/components/BrandPanel.jsx @@ -0,0 +1,52 @@ +import React from "react"; +import { Lock, Zap, BarChart3 } from "lucide-react"; + +const brandFeatures = [ + { icon: Lock, text: "Enterprise-grade security & encryption" }, + { icon: Zap, text: "Real-time compliance monitoring" }, + { icon: BarChart3, text: "Actionable reporting & insights" }, +]; + +const BrandPanel = () => { + return ( +
    + {Array.from({ length: 6 }).map((_, index) => ( + + ))} + +
    + + + AutoAudit + + +
    +

    Access security insights anywhere

    +

    + Connect to your Microsoft 365 compliance dashboard, monitor security posture, and act on + real-time recommendations. +

    +
    + +
    + {brandFeatures.map((feature) => { + const Icon = feature.icon; + return ( +
    + + {feature.text} +
    + ); + })} +
    +
    +
    + ); +}; + +export default BrandPanel; diff --git a/frontend/src/pages/Auth/components/LoginHeader.jsx b/frontend/src/pages/Auth/components/LoginHeader.jsx new file mode 100644 index 00000000..acdd21e8 --- /dev/null +++ b/frontend/src/pages/Auth/components/LoginHeader.jsx @@ -0,0 +1,29 @@ +import React from "react"; + +const navLinks = [ + { label: "Home", href: "/" }, + { label: "Features", href: "/#features" }, + { label: "Benefits", href: "/#benefits" }, + { label: "About", href: "/about" }, + { label: "Contact", href: "/contact" }, +]; + +const LoginHeader = () => { + return ( +
    + + AutoAudit + + + +
    + ); +}; + +export default LoginHeader; diff --git a/frontend/src/pages/Auth/components/SignInPanel.jsx b/frontend/src/pages/Auth/components/SignInPanel.jsx new file mode 100644 index 00000000..3936873f --- /dev/null +++ b/frontend/src/pages/Auth/components/SignInPanel.jsx @@ -0,0 +1,234 @@ +import React, { useState } from "react"; +import { + ArrowRight, + Eye, + EyeOff, + Lock, + Mail, + AlertCircle, + Loader2, +} from "lucide-react"; +import { useAuth } from "../../../context/AuthContext"; + +const socialButtons = [ + { + label: "Google", + provider: "google", + icon: ( + + ), + }, +]; + +const SignInPanel = ({ onLogin, onSignUpClick }) => { + const auth = useAuth(); + const [formData, setFormData] = useState({ + email: "", + password: "", + remember: true, + }); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // Vite exposes env vars via import.meta.env (and they must be prefixed with VITE_) + const apiBaseUrl = import.meta.env.VITE_API_URL; + + const handleChange = (event) => { + const { name, value, type, checked } = event.target; + setFormData((prev) => ({ + ...prev, + [name]: type === "checkbox" ? checked : value, + })); + // Clear error when user starts typing + if (error) setError(null); + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + setError(null); + setIsLoading(true); + + try { + if (onLogin) { + await onLogin(formData.email, formData.password, formData.remember); + } else { + await auth.login(formData.email, formData.password, formData.remember); + } + } catch (err) { + setError(err.message || "Login failed. Please check your credentials."); + } finally { + setIsLoading(false); + } + }; + + const handleSocialLogin = (provider) => { + if (isLoading) return; + setError(null); + + if (!apiBaseUrl) { + setError("Missing API configuration. Please set VITE_API_URL."); + return; + } + + if (provider === "google") { + // Backend-driven OAuth redirect flow. + // The callback will land on: /auth/google/callback#access_token=... + window.location.assign(`${apiBaseUrl}/v1/auth/google/authorize`); + return; + } + + setError("Unsupported provider."); + }; + + return ( +
    +
    +
    +

    Welcome Back

    +

    Sign in to access your compliance dashboard and security reports.

    +
    + +
    + {error && ( +
    + + {error} +
    + )} + +
    + +
    + + +
    +
    + +
    + +
    + + + +
    +
    + +
    + + + Forgot password? + +
    + + +
    + +
    + Or sign in with +
    + +
    + {socialButtons.map((button) => ( + + ))} +
    + +

    + Don't have an account?{" "} + +

    +
    +
    + ); +}; + +export default SignInPanel; diff --git a/frontend/src/pages/Auth/components/SignupBrandPanel.jsx b/frontend/src/pages/Auth/components/SignupBrandPanel.jsx new file mode 100644 index 00000000..2269c8fa --- /dev/null +++ b/frontend/src/pages/Auth/components/SignupBrandPanel.jsx @@ -0,0 +1,54 @@ +import React from "react"; +import { BarChart3, Lock, Zap } from "lucide-react"; + +const featureItems = [ + { icon: Zap, text: "Setup in minutes, not hours" }, + { icon: Lock, text: "Bank-level security & encryption" }, + { icon: BarChart3, text: "Real-time compliance monitoring" }, +]; + +const SignupBrandPanel = () => { + return ( +
    + {Array.from({ length: 6 }).map((_, index) => ( + + ))} + +
    + + + AutoAudit + + +
    +

    Start Your Compliance Journey

    +

    + Join organizations that rely on AutoAudit to monitor, analyze, and improve their + Microsoft 365 security posture. +

    +
    + +
    + {featureItems.map((item) => { + const Icon = item.icon; + return ( +
    + + {item.text} +
    + ); + })} +
    +
    +
    + ); +}; + +export default SignupBrandPanel; diff --git a/frontend/src/pages/Auth/components/SignupFormPanel.jsx b/frontend/src/pages/Auth/components/SignupFormPanel.jsx new file mode 100644 index 00000000..6433359a --- /dev/null +++ b/frontend/src/pages/Auth/components/SignupFormPanel.jsx @@ -0,0 +1,278 @@ +import React, { useState } from "react"; +import { ArrowRight, Eye, EyeOff, Mail, Building, User, ShieldCheck } from "lucide-react"; + +const TERMS_ERROR_MESSAGE = "Please agree to the terms and privacy policy"; +const PASSWORD_MISMATCH_MESSAGE = "These passwords do not match"; + +const inputFields = [ + { + name: "firstName", + label: "First Name", + icon: , + type: "text", + placeholder: "First name", + }, + { + name: "lastName", + label: "Last Name", + icon: , + type: "text", + placeholder: "Last name", + }, + { + name: "email", + label: "Email Address", + icon: , + type: "email", + placeholder: "your.email@company.com", + }, + { + name: "organizationName", + label: "Organization Name", + icon: , + type: "text", + placeholder: "Enter your organization name", + }, +]; + +const socialButtons = [ + { + label: "Google", + provider: "google", + icon: ( + + ), + }, +]; + +const SignupFormPanel = ({ formData, onFormChange, onSubmit, onBackToLogin, submitError }) => { + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [agreeTerms, setAgreeTerms] = useState(false); + const [error, setError] = useState(""); + // Vite exposes env vars via import.meta.env (and they must be prefixed with VITE_) + const apiBaseUrl = import.meta.env.VITE_API_URL; + + const handleAgreeTermsChange = (event) => { + const checked = event.target.checked; + setAgreeTerms(checked); + + // Clear stale “agree to terms” error as soon as the user fixes it. + if (checked && error === TERMS_ERROR_MESSAGE) { + setError(""); + } + }; + + const handleChange = (event) => { + const { name, value } = event.target; + onFormChange(name, value); + if (error) setError(""); + }; + + const validate = () => { + if (!agreeTerms) { + setError(TERMS_ERROR_MESSAGE); + return false; + } + if (formData.password !== formData.confirmPassword) { + setError(PASSWORD_MISMATCH_MESSAGE); + return false; + } + // Ensure any previous validation error is cleared before a successful submit. + if (error) setError(""); + return true; + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + if (!validate()) return; + await onSubmit({ ...formData, agreeTerms: true }); + }; + + const handleSocialSignUp = (provider) => { + if (!apiBaseUrl) { + setError("Missing API configuration. Please set VITE_API_URL."); + return; + } + + if (provider === "google") { + // Backend-driven OAuth redirect flow. + // The callback will land on: /auth/google/callback#access_token=... + window.location.assign(`${apiBaseUrl}/v1/auth/google/authorize`); + return; + } + + setError("Unsupported provider."); + }; + + return ( +
    +
    +
    +

    Create Account

    +

    Start your compliance journey with AutoAudit.

    +
    + +
    +
    + {inputFields.slice(0, 2).map((field) => ( + + ))} +
    + + {inputFields.slice(2).map((field) => ( + + ))} + + + + + + + + {(error || submitError) && ( +

    + {error || submitError} +

    + )} + + +
    + +
    + Or sign up with +
    + +
    + {socialButtons.map((button) => ( + + ))} +
    + +

    + Already have an account? +

    +
    +
    + ); +}; + +export default SignupFormPanel; diff --git a/frontend/src/pages/Connections/ConnectionsPage.css b/frontend/src/pages/Connections/ConnectionsPage.css new file mode 100644 index 00000000..5d51788e --- /dev/null +++ b/frontend/src/pages/Connections/ConnectionsPage.css @@ -0,0 +1,325 @@ +.connections-page { + min-height: 100vh; + /* Match Dashboard primary button accent */ + --teal: #4e94f8; + background: var(--bg-primary); + padding: 24px; + transition: background-color 0.3s ease; +} + +.connections-page.light { + --bg-primary: #f8fafc; + --bg-secondary: #ffffff; + --bg-tertiary: #e2e8f0; + --text-primary: #1e293b; + --text-secondary: #64748b; + --text-tertiary: #94a3b8; + --border-color: #e2e8f0; +} + +.connections-container { + max-width: 1000px; + margin: 0 auto; +} + +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.page-header .header-content { + display: flex; + align-items: center; + gap: 16px; + color: var(--text-primary); +} + +.page-header .header-content svg { + color: var(--teal, #64DFDF); +} + +.page-header .header-text h1 { + font-size: 24px; + font-weight: bold; + color: var(--text-primary); + margin: 0; +} + +.page-header .header-text p { + color: var(--text-secondary); + font-size: 14px; + margin: 0; +} + +.error-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 8px; + color: #ef4444; + margin-bottom: 24px; +} + +.connection-form-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; +} + +.connection-form-card h3 { + color: var(--text-primary); + font-size: 18px; + font-weight: 600; + margin: 0 0 20px 0; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + color: var(--text-secondary); + font-size: 14px; + font-weight: 500; + margin-bottom: 8px; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 10px 14px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + transition: border-color 0.2s ease; +} + +.form-group select option { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: var(--teal, #64DFDF); +} + +.form-group input::placeholder { + color: var(--text-tertiary); +} + +.form-group input:disabled, +.form-group select:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 8px; +} + +.connections-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.connection-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 20px; + display: flex; + align-items: center; + justify-content: space-between; + transition: border-color 0.2s ease; +} + +.connection-card:hover { + border-color: var(--teal, #64DFDF); +} + +.connection-info { + flex: 1; +} + +.connection-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.connection-header h4 { + color: var(--text-primary); + font-size: 16px; + font-weight: 600; + margin: 0; +} + +.status-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 20px; + font-size: 12px; + font-weight: 500; +} + +.status-badge.connected { + background: rgba(16, 185, 129, 0.15); + color: #10b981; +} + +.status-badge.failed { + background: rgba(239, 68, 68, 0.15); + color: #ef4444; +} + +.status-badge.pending { + background: rgba(249, 115, 22, 0.15); + color: #f97316; +} + +.status-icon.success { + color: #10b981; +} + +.status-icon.error { + color: #ef4444; +} + +.status-icon.pending { + color: #f97316; +} + +.connection-details { + display: flex; + flex-wrap: wrap; + gap: 16px; +} + +.detail-item { + color: var(--text-secondary); + font-size: 13px; +} + +.detail-item strong { + color: var(--text-tertiary); + font-weight: 500; +} + +.connection-actions { + display: flex; + gap: 8px; +} + +.toolbar-button.danger { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: #ef4444; +} + +.toolbar-button.danger:hover:not(:disabled) { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.5); +} + +.toolbar-button.danger:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + background: var(--bg-secondary); + border: 1px dashed var(--border-color); + border-radius: 12px; +} + +.empty-state svg { + color: var(--text-tertiary); + margin-bottom: 16px; +} + +.empty-state h3 { + color: var(--text-primary); + font-size: 18px; + font-weight: 600; + margin: 0 0 8px 0; +} + +.empty-state p { + color: var(--text-secondary); + font-size: 14px; + margin: 0; +} + +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + color: var(--text-secondary); +} + +.loading-state p { + margin-top: 16px; +} + +.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (max-width: 768px) { + .form-row { + grid-template-columns: 1fr; + } + + .connection-card { + flex-direction: column; + align-items: flex-start; + gap: 16px; + } + + .connection-actions { + width: 100%; + } + + .connection-actions button { + flex: 1; + justify-content: center; + } +} diff --git a/frontend/src/pages/Connections/ConnectionsPage.jsx b/frontend/src/pages/Connections/ConnectionsPage.jsx new file mode 100644 index 00000000..37e0ada1 --- /dev/null +++ b/frontend/src/pages/Connections/ConnectionsPage.jsx @@ -0,0 +1,551 @@ +import React, { useState, useEffect } from 'react'; +import { Plus, Link2, AlertCircle, Loader2, RefreshCw, Pencil, Trash2, CheckCircle2, XCircle } from 'lucide-react'; +import { useAuth } from '../../context/AuthContext'; +import { APIError, getPlatforms, getConnections, createConnection, updateConnection, deleteConnection, testConnection } from '../../api/client'; +import './ConnectionsPage.css'; + +const CLIENT_SECRET_MASK = '************'; + +const ConnectionsPage = ({ sidebarWidth = 220, isDarkMode = true }) => { + const { token } = useAuth(); + const [platforms, setPlatforms] = useState([]); + const [connections, setConnections] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [showForm, setShowForm] = useState(false); + const [formData, setFormData] = useState({ + name: '', + platform_id: '', + tenant_id: '', + client_id: '', + client_secret: '', + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [editingConnection, setEditingConnection] = useState(null); + const [editFormData, setEditFormData] = useState({ + name: '', + tenant_id: '', + client_id: '', + client_secret: '', + }); + const [isEditing, setIsEditing] = useState(false); + const [deletingId, setDeletingId] = useState(null); + const [testingId, setTestingId] = useState(null); + const [testResults, setTestResults] = useState({}); + + function getConnectionErrorMessage(err, fallbackMessage) { + // Backend returns 400 Bad Request when M365 auth cannot be established. + if (err instanceof APIError && err.status === 400) { + return 'Authentication not established. Please check your tenant ID, client ID, and client secret and try again.'; + } + if (err?.status === 400) { + return 'Authentication not established. Please check your tenant ID, client ID, and client secret and try again.'; + } + return err?.message || fallbackMessage; + } + + useEffect(() => { + loadData(); + }, [token]); + + async function loadData() { + setIsLoading(true); + setError(null); + try { + const [platformsData, connectionsData] = await Promise.all([ + getPlatforms(token), + getConnections(token), + ]); + setPlatforms(platformsData); + setConnections(connectionsData); + } catch (err) { + setError(err.message || 'Failed to load data'); + } finally { + setIsLoading(false); + } + } + + function handleChange(e) { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + } + + async function handleSubmit(e) { + e.preventDefault(); + setIsSubmitting(true); + setError(null); + + try { + const newConnection = await createConnection(token, { + name: formData.name, + tenant_id: formData.tenant_id, + client_id: formData.client_id, + client_secret: formData.client_secret, + }); + setConnections(prev => [...prev, newConnection]); + setFormData({ + name: '', + platform_id: '', + tenant_id: '', + client_id: '', + client_secret: '', + }); + setShowForm(false); + } catch (err) { + setError(getConnectionErrorMessage(err, 'Failed to create connection')); + } finally { + setIsSubmitting(false); + } + } + + async function handleTestConnection(connection) { + setTestingId(connection.id); + setError(null); + try { + const result = await testConnection(token, connection.id); + setTestResults(prev => ({ ...prev, [connection.id]: result })); + if (!result?.success) { + setError(result?.message || 'Connection test failed'); + } + } catch (err) { + const message = getConnectionErrorMessage(err, 'Connection test failed'); + setError(message); + setTestResults(prev => ({ + ...prev, + [connection.id]: { success: false, message }, + })); + } finally { + setTestingId(null); + } + } + + function startEditing(connection) { + setEditingConnection(connection); + setEditFormData({ + name: connection.name, + tenant_id: connection.tenant_id, + client_id: connection.client_id, + // Never expose the actual secret; show a mask so it doesn't look blank. + client_secret: CLIENT_SECRET_MASK, + }); + } + + function handleEditChange(e) { + const { name } = e.target; + let { value } = e.target; + + // If the user starts typing while the masked placeholder is present, + // ensure we don't keep the mask characters in state. + if ( + name === 'client_secret' && + editFormData.client_secret === CLIENT_SECRET_MASK && + value.startsWith(CLIENT_SECRET_MASK) + ) { + value = value.slice(CLIENT_SECRET_MASK.length); + } + setEditFormData(prev => ({ ...prev, [name]: value })); + } + + async function handleEditSubmit(e) { + e.preventDefault(); + setIsEditing(true); + setError(null); + try { + const updateData = { + name: editFormData.name, + tenant_id: editFormData.tenant_id, + client_id: editFormData.client_id, + }; + // Only include client_secret if user entered a new one + if (editFormData.client_secret && editFormData.client_secret !== CLIENT_SECRET_MASK) { + updateData.client_secret = editFormData.client_secret; + } + + const updatedConnection = await updateConnection(token, editingConnection.id, updateData); + setConnections(prev => + prev.map(conn => (conn.id === editingConnection.id ? updatedConnection : conn)) + ); + setEditingConnection(null); + } catch (err) { + setError(getConnectionErrorMessage(err, 'Failed to update connection')); + } finally { + setIsEditing(false); + } + } + + function cancelEditing() { + setEditingConnection(null); + setEditFormData({ + name: '', + tenant_id: '', + client_id: '', + client_secret: '', + }); + } + + async function handleDelete(id) { + if (!window.confirm('Are you sure you want to delete this connection? This action cannot be undone.')) { + return; + } + + setDeletingId(id); + setError(null); + + try { + await deleteConnection(token, id); + setConnections(prev => prev.filter(conn => conn.id !== id)); + } catch (err) { + setError(err.message || 'Failed to delete connection'); + } finally { + setDeletingId(null); + } + } + + if (isLoading) { + return ( +
    +
    +
    + +

    Loading connections...

    +
    +
    +
    + ); + } + + return ( +
    +
    +
    +
    + +
    +

    Cloud Platforms

    +

    Manage your cloud platform connections

    +
    +
    + +
    + + {error && ( +
    + + {error} +
    + )} + + {showForm && ( +
    +

    New Connection

    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    + )} + + {editingConnection && ( +
    +

    Edit Connection

    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + { + if (e.target.value === CLIENT_SECRET_MASK) { + e.target.select(); + } + }} + disabled={isEditing} + /> +
    + +
    + + +
    +
    +
    + )} + +
    + {connections.length === 0 ? ( +
    + +

    No connections yet

    +

    Add your first M365 connection to start scanning.

    +
    + ) : ( + connections.map(connection => ( +
    +
    +
    +

    {connection.name}

    + {testingId === connection.id ? ( + + + Testing + + ) : testResults[connection.id]?.success === true ? ( + + + Connected + + ) : testResults[connection.id]?.success === false ? ( + + + Failed + + ) : null} +
    +
    + + Tenant ID: {connection.tenant_id} + + + Client ID: {connection.client_id} + + {testResults[connection.id]?.tenant_display_name ? ( + + Tenant: {testResults[connection.id].tenant_display_name} + + ) : null} + {testResults[connection.id]?.default_domain ? ( + + Default Domain: {testResults[connection.id].default_domain} + + ) : null} +
    +
    +
    + + + +
    +
    + )) + )} +
    +
    +
    + ); +}; + +export default ConnectionsPage; diff --git a/frontend/src/pages/Contact/ContactPage.css b/frontend/src/pages/Contact/ContactPage.css new file mode 100644 index 00000000..54d4c8d5 --- /dev/null +++ b/frontend/src/pages/Contact/ContactPage.css @@ -0,0 +1,365 @@ +.contact-page { + --accent: #3b82f6; + --accent-strong: #2563eb; + --border: rgba(59, 130, 246, 0.1); + + background: #0a1628; + color: #ffffff; + min-height: 100vh; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +.contact-page main { + display: flex; + flex-direction: column; + gap: 0; +} + +.contact-hero { + padding: 10rem 5% 4rem; + background: linear-gradient(135deg, #0a1628 0%, #162a4a 50%, #1e3a5f 100%); + position: relative; + overflow: hidden; + text-align: center; +} + +.contact-hero::before, +.contact-hero::after { + content: ""; + position: absolute; + border-radius: 50%; + opacity: 0.9; + animation: pulse 8s ease-in-out infinite; +} + +.contact-hero::before { + width: 600px; + height: 600px; + background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%); + top: -200px; + right: -200px; +} + +.contact-hero::after { + width: 400px; + height: 400px; + background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%); + bottom: -100px; + left: -100px; + animation-duration: 6s; +} + +@keyframes pulse { + 0%, + 100% { + transform: scale(1); + opacity: 0.5; + } + 50% { + transform: scale(1.2); + opacity: 0.8; + } +} + +.contact-hero-content { + max-width: 800px; + margin: 0 auto; + position: relative; + z-index: 1; +} + +.contact-hero h1 { + font-size: clamp(2.5rem, 6vw, 3.5rem); + margin-bottom: 1rem; + background: linear-gradient(135deg, #ffffff, var(--accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.contact-hero p { + color: #b0c4de; + font-size: 1.2rem; + line-height: 1.6; +} + +.contact-section { + padding: 6rem 5%; + background: #0f1f38; +} + +.contact-container { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 1.4fr; + gap: 3rem; +} + +.contact-info { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.info-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + border-radius: 20px; + padding: 2rem; + transition: all 0.3s ease; +} + +.info-card:hover { + border-color: var(--accent); + background: rgba(255, 255, 255, 0.05); +} + +.info-icon { + width: 56px; + height: 56px; + border-radius: 14px; + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.8rem; + margin-bottom: 1.25rem; +} + +.info-card h3 { + font-size: 1.25rem; + margin-bottom: 0.5rem; +} + +.info-card p { + color: #b0c4de; + line-height: 1.6; + margin-bottom: 0.5rem; + white-space: pre-line; +} + +.contact-link { + display: block; + color: var(--accent); + text-decoration: none; + margin-top: 0.25rem; +} + +.contact-link:hover { + color: var(--accent-strong); +} + +.social-links { + display: flex; + gap: 1rem; + margin-top: 1rem; +} + +.social-link { + width: 44px; + height: 44px; + border-radius: 12px; + border: 1px solid rgba(59, 130, 246, 0.2); + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + color: #ffffff; + background: rgba(59, 130, 246, 0.12); + transition: all 0.3s ease; +} + +.social-link:hover { + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + border-color: transparent; +} + +.social-link svg { + width: 18px; + height: 18px; + display: block; +} + +.contact-form-wrapper { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + border-radius: 20px; + padding: 3rem; + backdrop-filter: blur(10px); +} + +.contact-form-wrapper h2 { + margin-bottom: 1.5rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #b0c4de; +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: 1rem; + border-radius: 12px; + border: 1px solid rgba(59, 130, 246, 0.2); + background: rgba(255, 255, 255, 0.05); + color: #ffffff; + font-size: 1rem; + font-family: inherit; +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: var(--accent); + background: rgba(255, 255, 255, 0.08); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); +} + +.form-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.25rem; +} + +.form-group textarea { + min-height: 150px; + resize: vertical; +} + +.submit-btn { + width: 100%; + margin-top: 0.5rem; + font-size: 1.1rem; +} + +.success-message { + display: none; + margin-top: 1rem; + padding: 1rem; + border-radius: 12px; + border: 1px solid var(--accent); + background: rgba(59, 130, 246, 0.12); + text-align: center; + color: var(--accent); +} + +.success-message.show { + display: block; + animation: slideIn 0.4s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.faq-section { + padding: 6rem 5%; + background: linear-gradient(135deg, #0a1628, #1e3a5f); +} + +.faq-container { + max-width: 900px; + margin: 0 auto; +} + +.faq-header { + text-align: center; + margin-bottom: 2.5rem; +} + +.faq-header h2 { + font-size: 2.4rem; + margin-bottom: 0.75rem; +} + +.faq-header p { + color: #b0c4de; +} + +.faq-item { + border: 1px solid var(--border); + border-radius: 15px; + margin-bottom: 1rem; + background: rgba(255, 255, 255, 0.02); + overflow: hidden; +} + +.faq-question { + width: 100%; + text-align: left; + background: transparent; + border: none; + padding: 1.25rem 1.5rem; + color: #ffffff; + font-size: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; +} + +.faq-question:hover { + background: rgba(255, 255, 255, 0.04); +} + +.faq-icon { + font-size: 1.4rem; + color: var(--accent); + transition: transform 0.3s ease; +} + +.faq-item.active .faq-icon { + transform: rotate(45deg); +} + +.faq-answer { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.faq-item.active .faq-answer { + max-height: 500px; +} + +.faq-answer-content { + padding: 0 1.5rem 1.5rem; + color: #b0c4de; + line-height: 1.6; +} + +@media (max-width: 1024px) { + .contact-container { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .contact-hero { + padding-top: 8rem; + } + + .form-row { + grid-template-columns: 1fr; + } + + .contact-form-wrapper { + padding: 2rem; + } +} diff --git a/frontend/src/pages/Contact/ContactPage.jsx b/frontend/src/pages/Contact/ContactPage.jsx new file mode 100644 index 00000000..66fc7bf6 --- /dev/null +++ b/frontend/src/pages/Contact/ContactPage.jsx @@ -0,0 +1,51 @@ +import React, { useState } from "react"; +import "./ContactPage.css"; +import "../Landing/LandingPage.css"; +import LandingHeader from "../Landing/components/LandingHeader"; +import LandingFooter from "../Landing/components/LandingFooter"; +import ContactInfoGrid from "./components/ContactInfoGrid"; +import ContactForm from "./components/ContactForm"; +import FAQSection from "./components/FAQSection"; + +const ContactHero = () => ( +
    +
    +

    Contact AutoAudit

    +

    Get in Touch

    +

    + Have questions about AutoAudit? Our team is here to help. Reach out and + we'll respond as soon as possible. +

    +
    +
    +); + +const ContactPage = ({ onSignIn }) => { + const [submitted, setSubmitted] = useState(false); + + const handleFormSuccess = () => { + setSubmitted(true); + setTimeout(() => setSubmitted(false), 5000); + }; + + return ( +
    + +
    + + +
    +
    + + +
    +
    + + +
    + +
    + ); +}; + +export default ContactPage; diff --git a/frontend/src/pages/Contact/components/ContactForm.jsx b/frontend/src/pages/Contact/components/ContactForm.jsx new file mode 100644 index 00000000..f79d8e3e --- /dev/null +++ b/frontend/src/pages/Contact/components/ContactForm.jsx @@ -0,0 +1,131 @@ +import React, { useState } from "react"; + +const initialState = { + firstName: "", + lastName: "", + email: "", + phone: "", + company: "", + subject: "", + message: "", +}; + +const ContactForm = ({ submitted, onSuccess }) => { + const [formData, setFormData] = useState(initialState); + + const handleChange = (event) => { + const { name, value } = event.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = (event) => { + event.preventDefault(); + onSuccess(); + setFormData(initialState); + }; + + return ( +
    +

    Send us a Message

    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    + +
    + + +
    + +
    + +