diff --git a/.env b/.env
index c761dd0fd0..f5c9802afe 100644
--- a/.env
+++ b/.env
@@ -145,7 +145,7 @@ SENTRY_RELEASE=local
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
#### Prowler release version ####
-NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.24.0
+NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.25.0
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b953610fa1..3300394d83 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,14 +1,15 @@
# SDK
-/* @prowler-cloud/sdk
-/prowler/ @prowler-cloud/sdk @prowler-cloud/detection-and-remediation
-/tests/ @prowler-cloud/sdk @prowler-cloud/detection-and-remediation
-/dashboard/ @prowler-cloud/sdk
-/docs/ @prowler-cloud/sdk
-/examples/ @prowler-cloud/sdk
-/util/ @prowler-cloud/sdk
-/contrib/ @prowler-cloud/sdk
-/permissions/ @prowler-cloud/sdk
-/codecov.yml @prowler-cloud/sdk @prowler-cloud/api
+/* @prowler-cloud/detection-remediation
+/prowler/ @prowler-cloud/detection-remediation
+/prowler/compliance/ @prowler-cloud/compliance
+/tests/ @prowler-cloud/detection-remediation
+/dashboard/ @prowler-cloud/detection-remediation
+/docs/ @prowler-cloud/detection-remediation
+/examples/ @prowler-cloud/detection-remediation
+/util/ @prowler-cloud/detection-remediation
+/contrib/ @prowler-cloud/detection-remediation
+/permissions/ @prowler-cloud/detection-remediation
+/codecov.yml @prowler-cloud/detection-remediation @prowler-cloud/api
# API
/api/ @prowler-cloud/api
@@ -17,7 +18,7 @@
/ui/ @prowler-cloud/ui
# AI
-/mcp_server/ @prowler-cloud/ai
+/mcp_server/ @prowler-cloud/detection-remediation
# Platform
/.github/ @prowler-cloud/platform
diff --git a/.github/actions/setup-python-poetry/action.yml b/.github/actions/setup-python-poetry/action.yml
index fd96796b9b..dd5be4b3a2 100644
--- a/.github/actions/setup-python-poetry/action.yml
+++ b/.github/actions/setup-python-poetry/action.yml
@@ -64,19 +64,6 @@ runs:
echo "Updated resolved_reference:"
grep -A2 -B2 "resolved_reference" poetry.lock
- - name: Update SDK resolved_reference to latest commit (prowler repo on push)
- if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == 'prowler-cloud/prowler'
- shell: bash
- working-directory: ${{ inputs.working-directory }}
- run: |
- LATEST_COMMIT=$(curl -s "https://api.github.com/repos/prowler-cloud/prowler/commits/master" | jq -r '.sha')
- echo "Latest commit hash: $LATEST_COMMIT"
- sed -i '/url = "https:\/\/github\.com\/prowler-cloud\/prowler\.git"/,/resolved_reference = / {
- s/resolved_reference = "[a-f0-9]\{40\}"/resolved_reference = "'"$LATEST_COMMIT"'"/
- }' poetry.lock
- echo "Updated resolved_reference:"
- grep -A2 -B2 "resolved_reference" poetry.lock
-
- name: Update poetry.lock (prowler repo only)
if: github.repository == 'prowler-cloud/prowler' && inputs.update-lock == 'true'
shell: bash
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 28eff02ff6..953c0742f6 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -66,6 +66,18 @@ updates:
cooldown:
default-days: 7
+ - package-ecosystem: "pre-commit"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ open-pull-requests-limit: 25
+ target-branch: master
+ labels:
+ - "dependencies"
+ - "pre-commit"
+ cooldown:
+ default-days: 7
+
# Dependabot Updates are temporary disabled - 2025/04/15
# v4.6
# - package-ecosystem: "pip"
diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml
index 0aa955413f..88f84d6729 100644
--- a/.github/workflows/find-secrets.yml
+++ b/.github/workflows/find-secrets.yml
@@ -27,11 +27,12 @@ jobs:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
- egress-policy: block
- allowed-endpoints: >
- github.com:443
- ghcr.io:443
- pkg-containers.githubusercontent.com:443
+ # We can't block as Trufflehog needs to verify secrets against vendors
+ egress-policy: audit
+ # allowed-endpoints: >
+ # github.com:443
+ # ghcr.io:443
+ # pkg-containers.githubusercontent.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
diff --git a/.gitignore b/.gitignore
index 8686f4f143..d959c0e524 100644
--- a/.gitignore
+++ b/.gitignore
@@ -84,6 +84,7 @@ continue.json
.continuerc.json
# AI Coding Assistants - OpenCode
+.opencode/
opencode.json
# AI Coding Assistants - GitHub Copilot
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 633c10a80f..c7d3f86f3b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,7 @@
repos:
## GENERAL
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
+ rev: v6.0.0
hooks:
- id: check-merge-conflict
- id: check-yaml
@@ -16,7 +16,7 @@ repos:
## TOML
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
- rev: v2.13.0
+ rev: v2.16.0
hooks:
- id: pretty-format-toml
args: [--autofix]
@@ -24,21 +24,21 @@ repos:
## GITHUB ACTIONS
- repo: https://github.com/zizmorcore/zizmor-pre-commit
- rev: v1.6.0
+ rev: v1.24.1
hooks:
- id: zizmor
files: ^\.github/
## BASH
- repo: https://github.com/koalaman/shellcheck-precommit
- rev: v0.10.0
+ rev: v0.11.0
hooks:
- id: shellcheck
exclude: contrib
## PYTHON
- repo: https://github.com/myint/autoflake
- rev: v2.3.1
+ rev: v2.3.3
hooks:
- id: autoflake
exclude: ^skills/
@@ -50,20 +50,20 @@ repos:
]
- repo: https://github.com/pycqa/isort
- rev: 5.13.2
+ rev: 8.0.1
hooks:
- id: isort
exclude: ^skills/
args: ["--profile", "black"]
- repo: https://github.com/psf/black
- rev: 24.4.2
+ rev: 26.3.1
hooks:
- id: black
exclude: ^skills/
- repo: https://github.com/pycqa/flake8
- rev: 7.0.0
+ rev: 7.3.0
hooks:
- id: flake8
exclude: (contrib|^skills/)
@@ -93,7 +93,7 @@ repos:
pass_filenames: false
- repo: https://github.com/hadolint/hadolint
- rev: v2.13.0-beta
+ rev: v2.14.0
hooks:
- id: hadolint
args: ["--ignore=DL3013"]
diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md
index ff44ef24f0..a1f73e0700 100644
--- a/api/CHANGELOG.md
+++ b/api/CHANGELOG.md
@@ -2,7 +2,34 @@
All notable changes to the **Prowler API** are documented in this file.
-## [1.25.0] (Prowler UNRELEASED)
+## [1.25.2] (Prowler v5.24.2)
+
+### 🔄 Changed
+
+- Finding groups `/resources` endpoints now materialize the filtered finding IDs into a Python list before filtering `ResourceFindingMapping`, so PostgreSQL switches from a Merge Semi Join that read hundreds of thousands of RFM index entries to a Nested Loop Index Scan over `finding_id`. The `has_mappings.exists()` pre-check is removed, and a request-scoped cache deduplicates the finding-id round-trip across the helpers that build different RFM querysets [(#10816)](https://github.com/prowler-cloud/prowler/pull/10816)
+
+### 🐞 Fixed
+
+- `/finding-groups/latest//resources` now selects the latest completed scan per provider by `-completed_at` (then `-inserted_at`) instead of `-inserted_at`, matching the `/finding-groups/latest` summary path and the daily-summary upsert so overlapping scans no longer produce diverging `delta`/`new_count` between the two endpoints [(#10802)](https://github.com/prowler-cloud/prowler/pull/10802)
+
+---
+
+## [1.25.1] (Prowler v5.24.1)
+
+### 🔄 Changed
+
+- Attack Paths: Restore `SYNC_BATCH_SIZE` and `FINDINGS_BATCH_SIZE` defaults to 1000, upgrade Cartography to 0.135.0, enable Celery queue priority for cleanup task, rewrite Finding insertion, remove AWS graph cleanup and add timing logs [(#10729)](https://github.com/prowler-cloud/prowler/pull/10729)
+
+### 🐞 Fixed
+
+- Finding group resources endpoints now include findings without associated resources (orphaned IaC findings) as simulated resource rows, and return one row per finding when multiple findings share a resource [(#10708)](https://github.com/prowler-cloud/prowler/pull/10708)
+- Attack Paths: Missing `tenant_id` filter while getting related findings after scan completes [(#10722)](https://github.com/prowler-cloud/prowler/pull/10722)
+- Finding group counters `pass_count`, `fail_count` and `manual_count` now exclude muted findings [(#10753)](https://github.com/prowler-cloud/prowler/pull/10753)
+- Silent data loss in `ResourceFindingMapping` bulk insert that left findings orphaned when `INSERT ... ON CONFLICT DO NOTHING` dropped rows without raising; added explicit `unique_fields` [(#10724)](https://github.com/prowler-cloud/prowler/pull/10724)
+
+---
+
+## [1.25.0] (Prowler v5.24.0)
### 🔄 Changed
diff --git a/api/poetry.lock b/api/poetry.lock
index 5e0ca3cb09..6bad57a758 100644
--- a/api/poetry.lock
+++ b/api/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -682,21 +682,21 @@ requests = ">=2.21.0,<3.0.0"
[[package]]
name = "alibabacloud-tea-openapi"
-version = "0.4.1"
+version = "0.4.4"
description = "Alibaba Cloud openapi SDK Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
- {file = "alibabacloud_tea_openapi-0.4.1-py3-none-any.whl", hash = "sha256:e46bfa3ca34086d2c357d217a0b7284ecbd4b3bab5c88e075e73aec637b0e4a0"},
- {file = "alibabacloud_tea_openapi-0.4.1.tar.gz", hash = "sha256:2384b090870fdb089c3c40f3fb8cf0145b8c7d6c14abbac521f86a01abb5edaf"},
+ {file = "alibabacloud_tea_openapi-0.4.4-py3-none-any.whl", hash = "sha256:cea6bc1fe35b0319a8752cb99eb0ecb0dab7ca1a71b99c12970ba0867410995f"},
+ {file = "alibabacloud_tea_openapi-0.4.4.tar.gz", hash = "sha256:1b0917bc03cd49417da64945e92731716d53e2eb8707b235f54e45b7473221ce"},
]
[package.dependencies]
alibabacloud-credentials = ">=1.0.2,<2.0.0"
alibabacloud-gateway-spi = ">=0.0.2,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
-cryptography = ">=3.0.0,<45.0.0"
+cryptography = {version = ">=3.0.0,<47.0.0", markers = "python_version >= \"3.8\""}
darabonba-core = ">=1.0.3,<2.0.0"
[[package]]
@@ -1526,19 +1526,19 @@ typing-extensions = ">=4.6.0"
[[package]]
name = "azure-mgmt-resource"
-version = "23.3.0"
+version = "24.0.0"
description = "Microsoft Azure Resource Management Client Library for Python"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "azure_mgmt_resource-23.3.0-py3-none-any.whl", hash = "sha256:ab216ee28e29db6654b989746e0c85a1181f66653929d2cb6e48fba66d9af323"},
- {file = "azure_mgmt_resource-23.3.0.tar.gz", hash = "sha256:fc4f1fd8b6aad23f8af4ed1f913df5f5c92df117449dc354fea6802a2829fea4"},
+ {file = "azure_mgmt_resource-24.0.0-py3-none-any.whl", hash = "sha256:27b32cd223e2784269f5a0db3c282042886ee4072d79cedc638438ece7cd0df4"},
+ {file = "azure_mgmt_resource-24.0.0.tar.gz", hash = "sha256:cf6b8995fcdd407ac9ff1dd474087129429a1d90dbb1ac77f97c19b96237b265"},
]
[package.dependencies]
azure-common = ">=1.1"
-azure-mgmt-core = ">=1.3.2"
+azure-mgmt-core = ">=1.5.0"
isodate = ">=0.6.1"
typing-extensions = ">=4.6.0"
@@ -1822,19 +1822,19 @@ crt = ["awscrt (==0.27.6)"]
[[package]]
name = "cartography"
-version = "0.132.0"
+version = "0.135.0"
description = "Explore assets and their relationships across your technical infrastructure."
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
- {file = "cartography-0.132.0-py3-none-any.whl", hash = "sha256:c070aa51d0ab4479cb043cae70b35e7df49f2fb5f1fa95ccf10000bbeb952262"},
- {file = "cartography-0.132.0.tar.gz", hash = "sha256:7c6332bc57fd2629d7b83aee7bd95a7b2edb0d51ef746efa0461399e0b66625c"},
+ {file = "cartography-0.135.0-py3-none-any.whl", hash = "sha256:c62c32a6917b8f23a8b98fe2b6c7c4a918b50f55918482966c4dae1cf5f538e1"},
+ {file = "cartography-0.135.0.tar.gz", hash = "sha256:3f500cd22c3b392d00e8b49f62acc95fd4dcd559ce514aafe2eb8101133c7a49"},
]
[package.dependencies]
adal = ">=1.2.4"
-aioboto3 = ">=13.0.0"
+aioboto3 = ">=15.0.0"
azure-cli-core = ">=2.26.0"
azure-identity = ">=1.5.0"
azure-keyvault-certificates = ">=4.0.0"
@@ -1852,9 +1852,9 @@ azure-mgmt-keyvault = ">=10.0.0"
azure-mgmt-logic = ">=10.0.0"
azure-mgmt-monitor = ">=3.0.0"
azure-mgmt-network = ">=25.0.0"
-azure-mgmt-resource = ">=10.2.0,<25.0.0"
+azure-mgmt-resource = ">=24.0.0,<25"
azure-mgmt-security = ">=5.0.0"
-azure-mgmt-sql = ">=3.0.1,<4"
+azure-mgmt-sql = ">=3.0.1"
azure-mgmt-storage = ">=16.0.0"
azure-mgmt-synapse = ">=2.0.0"
azure-mgmt-web = ">=7.0.0"
@@ -1862,38 +1862,39 @@ azure-synapse-artifacts = ">=0.17.0"
backoff = ">=2.1.2"
boto3 = ">=1.15.1"
botocore = ">=1.18.1"
-cloudflare = ">=4.1.0,<5.0.0"
+cloudflare = ">=4.1.0"
crowdstrike-falconpy = ">=0.5.1"
-cryptography = "*"
-dnspython = ">=1.15.0"
-duo-client = "*"
-google-api-python-client = ">=1.7.8"
+cryptography = ">=45.0.0"
+dnspython = ">=2.0.0"
+duo-client = ">=5.5.0"
+google-api-python-client = ">=2.0.0"
google-auth = ">=2.37.0"
google-cloud-asset = ">=1.0.0"
google-cloud-resource-manager = ">=1.14.2"
httpx = ">=0.24.0"
kubernetes = ">=22.6.0"
-marshmallow = ">=3.0.0rc7"
-msgraph-sdk = "*"
+marshmallow = ">=4.0.0"
+msgraph-sdk = ">=1.53.0"
msrestazure = ">=0.6.4"
neo4j = ">=6.0.0"
oci = ">=2.71.0"
okta = "<1.0.0"
-packageurl-python = "*"
-packaging = "*"
+packageurl-python = ">=0.17.0"
+packaging = ">=26.0.0"
pagerduty = ">=4.0.1"
policyuniverse = ">=1.1.0.0"
PyJWT = {version = ">=2.0.0", extras = ["crypto"]}
-python-dateutil = "*"
+python-dateutil = ">=2.9.0"
python-digitalocean = ">=1.16.0"
pyyaml = ">=5.3.1"
requests = ">=2.22.0"
scaleway = ">=2.10.0"
slack-sdk = ">=3.37.0"
-statsd = "*"
+statsd = ">=4.0.0"
typer = ">=0.9.0"
-types-aiobotocore-ecr = "*"
-xmltodict = "*"
+types-aiobotocore-ecr = ">=3.1.0"
+workos = ">=5.44.0"
+xmltodict = ">=1.0.0"
[[package]]
name = "celery"
@@ -2503,62 +2504,74 @@ dev = ["bandit", "coverage", "flake8", "pydocstyle", "pylint", "pytest", "pytest
[[package]]
name = "cryptography"
-version = "44.0.3"
+version = "46.0.6"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
-python-versions = "!=3.9.0,!=3.9.1,>=3.7"
+python-versions = "!=3.9.0,!=3.9.1,>=3.8"
groups = ["main", "dev"]
files = [
- {file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"},
- {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"},
- {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"},
- {file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"},
- {file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"},
- {file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"},
- {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"},
- {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"},
- {file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"},
- {file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"},
- {file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"},
+ {file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"},
+ {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"},
+ {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a"},
+ {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175"},
+ {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463"},
+ {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97"},
+ {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c"},
+ {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507"},
+ {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19"},
+ {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738"},
+ {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c"},
+ {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f"},
+ {file = "cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2"},
+ {file = "cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124"},
+ {file = "cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275"},
+ {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4"},
+ {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b"},
+ {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707"},
+ {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361"},
+ {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b"},
+ {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca"},
+ {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013"},
+ {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4"},
+ {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a"},
+ {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d"},
+ {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736"},
+ {file = "cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed"},
+ {file = "cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4"},
+ {file = "cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a"},
+ {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8"},
+ {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77"},
+ {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290"},
+ {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410"},
+ {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d"},
+ {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70"},
+ {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d"},
+ {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa"},
+ {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58"},
+ {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb"},
+ {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72"},
+ {file = "cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c"},
+ {file = "cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f"},
+ {file = "cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead"},
+ {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8"},
+ {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0"},
+ {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b"},
+ {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a"},
+ {file = "cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e"},
+ {file = "cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759"},
]
[package.dependencies]
-cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
+cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
[package.extras]
-docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
+docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
-nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
-pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
+nox = ["nox[uv] (>=2024.4.15)"]
+pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
+test = ["certifi (>=2024)", "cryptography-vectors (==46.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]]
@@ -2961,7 +2974,7 @@ files = [
[package.dependencies]
autopep8 = "*"
Django = ">=4.2"
-gprof2dot = ">=2017.9.19"
+gprof2dot = ">=2017.09.19"
sqlparse = "*"
[[package]]
@@ -3740,19 +3753,19 @@ urllib3 = ["packaging", "urllib3"]
[[package]]
name = "google-auth-httplib2"
-version = "0.2.1"
+version = "0.2.0"
description = "Google Authentication Library: httplib2 transport"
optional = false
-python-versions = ">=3.7"
+python-versions = "*"
groups = ["main"]
files = [
- {file = "google_auth_httplib2-0.2.1-py3-none-any.whl", hash = "sha256:1be94c611db91c01f9703e7f62b0a59bbd5587a95571c7b6fade510d648bc08b"},
- {file = "google_auth_httplib2-0.2.1.tar.gz", hash = "sha256:5ef03be3927423c87fb69607b42df23a444e434ddb2555b73b3679793187b7de"},
+ {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"},
+ {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"},
]
[package.dependencies]
-google-auth = ">=1.32.0,<3.0.0"
-httplib2 = ">=0.19.0,<1.0.0"
+google-auth = "*"
+httplib2 = ">=0.19.0"
[[package]]
name = "google-cloud-access-context-manager"
@@ -4569,7 +4582,7 @@ files = [
[package.dependencies]
attrs = ">=22.2.0"
-jsonschema-specifications = ">=2023.3.6"
+jsonschema-specifications = ">=2023.03.6"
referencing = ">=0.28.4"
rpds-py = ">=0.7.1"
@@ -4777,7 +4790,7 @@ librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""]
mongodb = ["pymongo (==4.15.3)"]
msgpack = ["msgpack (==1.1.2)"]
pyro = ["pyro4 (==4.82)"]
-qpid = ["qpid-python (==1.36.0.post1)", "qpid-tools (==1.36.0.post1)"]
+qpid = ["qpid-python (==1.36.0-1)", "qpid-tools (==1.36.0-1)"]
redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<6.5)"]
slmq = ["softlayer_messaging (>=1.0.3)"]
sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"]
@@ -4798,7 +4811,7 @@ files = [
]
[package.dependencies]
-certifi = ">=14.5.14"
+certifi = ">=14.05.14"
durationpy = ">=0.7"
google-auth = ">=1.0.1"
oauthlib = ">=3.2.2"
@@ -5181,24 +5194,16 @@ files = [
[[package]]
name = "marshmallow"
-version = "3.26.2"
+version = "4.3.0"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
- {file = "marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73"},
- {file = "marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57"},
+ {file = "marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46"},
+ {file = "marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880"},
]
-[package.dependencies]
-packaging = ">=17.0"
-
-[package.extras]
-dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"]
-docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"]
-tests = ["pytest", "simplejson"]
-
[[package]]
name = "matplotlib"
version = "3.10.8"
@@ -5492,14 +5497,14 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"]
[[package]]
name = "msgraph-sdk"
-version = "1.23.0"
+version = "1.55.0"
description = "The Microsoft Graph Python SDK"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "msgraph_sdk-1.23.0-py3-none-any.whl", hash = "sha256:58e0047b4ca59fd82022c02cd73fec0170a3d84f3b76721e3db2a0314df9a58a"},
- {file = "msgraph_sdk-1.23.0.tar.gz", hash = "sha256:6dd1ba9a46f5f0ce8599fd9610133adbd9d1493941438b5d3632fce9e55ed607"},
+ {file = "msgraph_sdk-1.55.0-py3-none-any.whl", hash = "sha256:c8e68ebc4b88af5111de312e7fa910a4e76ddf48a4534feadb1fb8a411c48cfc"},
+ {file = "msgraph_sdk-1.55.0.tar.gz", hash = "sha256:6df691a31954a050d26b8a678968017e157d940fb377f2a8a4e17a9741b98756"},
]
[package.dependencies]
@@ -5925,23 +5930,24 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "oci"
-version = "2.160.3"
+version = "2.169.0"
description = "Oracle Cloud Infrastructure Python SDK"
optional = false
python-versions = "*"
groups = ["main"]
files = [
- {file = "oci-2.160.3-py3-none-any.whl", hash = "sha256:858bff3e697098bdda44833d2476bfb4632126f0182178e7dbde4dbd156d71f0"},
- {file = "oci-2.160.3.tar.gz", hash = "sha256:57514889be3b713a8385d86e3ba8a33cf46e3563c2a7e29a93027fb30b8a2537"},
+ {file = "oci-2.169.0-py3-none-any.whl", hash = "sha256:c71bb5143f307791082b3e33cc1545c2490a518cfed85ab1948ef5107c36d30b"},
+ {file = "oci-2.169.0.tar.gz", hash = "sha256:f3c5fff00b01783b5325ea7b13bf140053ec1e9f41da20bfb9c8a349ee7662fa"},
]
[package.dependencies]
certifi = "*"
circuitbreaker = {version = ">=1.3.1,<3.0.0", markers = "python_version >= \"3.7\""}
-cryptography = ">=3.2.1,<46.0.0"
-pyOpenSSL = ">=17.5.0,<25.0.0"
+cryptography = ">=3.2.1,<47.0.0"
+pyOpenSSL = ">=17.5.0,<27.0.0"
python-dateutil = ">=2.5.3,<3.0.0"
pytz = ">=2016.10"
+urllib3 = {version = ">=2.6.3", markers = "python_version >= \"3.10.0\""}
[package.extras]
adk = ["docstring-parser (>=0.16) ; python_version >= \"3.10\" and python_version < \"4\"", "mcp (>=1.6.0) ; python_version >= \"3.10\" and python_version < \"4\"", "pydantic (>=2.10.6) ; python_version >= \"3.10\" and python_version < \"4\"", "rich (>=13.9.4) ; python_version >= \"3.10\" and python_version < \"4\""]
@@ -6659,7 +6665,7 @@ files = [
[[package]]
name = "prowler"
-version = "5.23.0"
+version = "5.25.0"
description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks."
optional = false
python-versions = ">=3.10,<3.13"
@@ -6679,7 +6685,7 @@ alibabacloud-rds20140815 = "12.0.0"
alibabacloud_sas20181203 = "6.1.0"
alibabacloud-sls20201230 = "5.9.0"
alibabacloud_sts20150401 = "1.1.6"
-alibabacloud_tea_openapi = "0.4.1"
+alibabacloud_tea_openapi = "0.4.4"
alibabacloud_vpc20160428 = "6.13.0"
alive-progress = "3.3.0"
awsipranges = "0.3.3"
@@ -6701,7 +6707,7 @@ azure-mgmt-postgresqlflexibleservers = "1.1.0"
azure-mgmt-rdbms = "10.1.0"
azure-mgmt-recoveryservices = "3.1.0"
azure-mgmt-recoveryservicesbackup = "9.2.0"
-azure-mgmt-resource = "23.3.0"
+azure-mgmt-resource = "24.0.0"
azure-mgmt-search = "9.1.0"
azure-mgmt-security = "7.0.0"
azure-mgmt-sql = "3.0.1"
@@ -6714,29 +6720,29 @@ boto3 = "1.40.61"
botocore = "1.40.61"
cloudflare = "4.3.1"
colorama = "0.4.6"
-cryptography = "44.0.3"
+cryptography = "46.0.6"
dash = "3.1.1"
dash-bootstrap-components = "2.0.3"
-defusedxml = ">=0.7.1"
+defusedxml = "0.7.1"
detect-secrets = "1.5.0"
dulwich = "0.23.0"
google-api-python-client = "2.163.0"
-google-auth-httplib2 = ">=0.1,<0.3"
+google-auth-httplib2 = "0.2.0"
h2 = "4.3.0"
jsonschema = "4.23.0"
kubernetes = "32.0.1"
markdown = "3.10.2"
microsoft-kiota-abstractions = "1.9.2"
-msgraph-sdk = "1.23.0"
+msgraph-sdk = "1.55.0"
numpy = "2.0.2"
-oci = "2.160.3"
+oci = "2.169.0"
openstacksdk = "4.2.0"
pandas = "2.2.3"
py-iam-expand = "0.1.0"
py-ocsf-models = "0.8.1"
-pydantic = ">=2.0,<3.0"
+pydantic = "2.12.5"
pygithub = "2.8.0"
-python-dateutil = ">=2.9.0.post0,<3.0.0"
+python-dateutil = "2.9.0.post0"
pytz = "2025.1"
schema = "0.7.5"
shodan = "1.31.0"
@@ -6749,7 +6755,7 @@ uuid6 = "2024.7.10"
type = "git"
url = "https://github.com/prowler-cloud/prowler.git"
reference = "master"
-resolved_reference = "6ac90eb1b58590b6f2f51645dbef17b9231053f4"
+resolved_reference = "ca29e354b622198ff6a70e2ea5eb04e4a44a0903"
[[package]]
name = "psutil"
@@ -6914,14 +6920,14 @@ pydantic = ">=2.12.0,<3.0.0"
[[package]]
name = "pyasn1"
-version = "0.6.2"
+version = "0.6.3"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf"},
- {file = "pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b"},
+ {file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"},
+ {file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"},
]
[[package]]
@@ -6958,11 +6964,11 @@ description = "C parser in Python"
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
-markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
files = [
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
{file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
]
+markers = {main = "implementation_name != \"PyPy\" and platform_python_implementation != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""}
[[package]]
name = "pydantic"
@@ -7188,7 +7194,7 @@ files = [
]
[package.dependencies]
-astroid = ">=3.2.2,<=3.3.0.dev0"
+astroid = ">=3.2.2,<=3.3.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
@@ -7288,18 +7294,19 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "
[[package]]
name = "pyopenssl"
-version = "24.3.0"
+version = "26.0.0"
description = "Python wrapper module around the OpenSSL library"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"},
- {file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"},
+ {file = "pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81"},
+ {file = "pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc"},
]
[package.dependencies]
-cryptography = ">=41.0.5,<45"
+cryptography = ">=46.0.0,<47"
+typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""}
[package.extras]
docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"]
@@ -8202,10 +8209,10 @@ files = [
]
[package.dependencies]
-botocore = ">=1.37.4,<2.0a0"
+botocore = ">=1.37.4,<2.0a.0"
[package.extras]
-crt = ["botocore[crt] (>=1.37.4,<2.0a0)"]
+crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
[[package]]
name = "safety"
@@ -8807,6 +8814,23 @@ markupsafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
+[[package]]
+name = "workos"
+version = "6.0.4"
+description = "WorkOS Python Client"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "workos-6.0.4-py3-none-any.whl", hash = "sha256:548668b3702673536f853ba72a7b5bbbc269e467aaf9ac4f477b6e0177df5e21"},
+ {file = "workos-6.0.4.tar.gz", hash = "sha256:b0bfe8fd212b8567422c4ea3732eb33608794033eb3a69900c6b04db183c32d6"},
+]
+
+[package.dependencies]
+cryptography = ">=46.0,<47.0"
+httpx = ">=0.28,<1.0"
+pyjwt = ">=2.12,<3.0"
+
[[package]]
name = "wrapt"
version = "1.17.3"
@@ -9400,4 +9424,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<3.13"
-content-hash = "077e89853cfe3a6d934841488cfa5a98ff6c92b71f74b817b71387d11559f143"
+content-hash = "a3ab982d11a87d951ff15694d2ca7fd51f1f51a451abb0baa067ccf6966367a8"
diff --git a/api/pyproject.toml b/api/pyproject.toml
index 19827f2480..398936d62c 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -38,7 +38,7 @@ dependencies = [
"matplotlib (==3.10.8)",
"reportlab (==4.4.10)",
"neo4j (==6.1.0)",
- "cartography (==0.132.0)",
+ "cartography (==0.135.0)",
"gevent (==25.9.1)",
"werkzeug (==3.1.7)",
"sqlparse (==0.5.5)",
@@ -50,7 +50,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
-version = "1.25.0"
+version = "1.26.0"
[project.scripts]
celery = "src.backend.config.settings.celery"
@@ -62,7 +62,6 @@ django-silk = "5.3.2"
docker = "7.1.0"
filelock = "3.20.3"
freezegun = "1.5.1"
-marshmallow = "==3.26.2"
mypy = "1.10.1"
pylint = "3.2.5"
pytest = "9.0.3"
diff --git a/api/src/backend/api/migrations/0090_attack_paths_cleanup_priority.py b/api/src/backend/api/migrations/0090_attack_paths_cleanup_priority.py
new file mode 100644
index 0000000000..5ef8529b08
--- /dev/null
+++ b/api/src/backend/api/migrations/0090_attack_paths_cleanup_priority.py
@@ -0,0 +1,23 @@
+from django.db import migrations
+
+TASK_NAME = "attack-paths-cleanup-stale-scans"
+
+
+def set_cleanup_priority(apps, schema_editor):
+ PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")
+ PeriodicTask.objects.filter(name=TASK_NAME).update(priority=0)
+
+
+def unset_cleanup_priority(apps, schema_editor):
+ PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")
+ PeriodicTask.objects.filter(name=TASK_NAME).update(priority=None)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("api", "0089_backfill_finding_group_status_muted"),
+ ]
+
+ operations = [
+ migrations.RunPython(set_cleanup_priority, unset_cleanup_priority),
+ ]
diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml
index 816790de07..c7725e61df 100644
--- a/api/src/backend/api/specs/v1.yaml
+++ b/api/src/backend/api/specs/v1.yaml
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
- version: 1.25.0
+ version: 1.26.0
description: |-
Prowler API specification.
diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py
index 2249a041ab..3d2107a03e 100644
--- a/api/src/backend/api/tests/test_views.py
+++ b/api/src/backend/api/tests/test_views.py
@@ -57,6 +57,7 @@ from api.models import (
ProviderGroupMembership,
ProviderSecret,
Resource,
+ ResourceFindingMapping,
Role,
RoleProviderGroupRelationship,
SAMLConfiguration,
@@ -15465,7 +15466,7 @@ class TestFindingGroupViewSet:
attrs = data[0]["attributes"]
assert attrs["status"] == "FAIL"
assert attrs["muted"] is True
- assert attrs["fail_count"] == 2
+ assert attrs["fail_count"] == 0
assert attrs["fail_muted_count"] == 2
assert attrs["pass_muted_count"] == 0
assert attrs["manual_muted_count"] == 0
@@ -16030,6 +16031,36 @@ class TestFindingGroupViewSet:
# s3_bucket_public_access has 2 findings with 2 different resources
assert len(data) == 2
+ def test_resources_id_matches_resource_id_for_mapped_findings(
+ self, authenticated_client, finding_groups_fixture
+ ):
+ """Findings with a resource expose the resource id as row id (hot path contract)."""
+ response = authenticated_client.get(
+ reverse(
+ "finding-group-resources", kwargs={"pk": "s3_bucket_public_access"}
+ ),
+ {"filter[inserted_at]": TODAY},
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ data = response.json()["data"]
+ assert data, "expected resources in response"
+
+ resource_ids = set(
+ ResourceFindingMapping.objects.filter(
+ finding__check_id="s3_bucket_public_access",
+ ).values_list("resource_id", flat=True)
+ )
+ finding_ids = set(
+ Finding.objects.filter(
+ check_id="s3_bucket_public_access",
+ ).values_list("id", flat=True)
+ )
+
+ returned_ids = {item["id"] for item in data}
+ assert returned_ids <= {str(rid) for rid in resource_ids}
+ assert returned_ids.isdisjoint({str(fid) for fid in finding_ids})
+
def test_resources_fields(self, authenticated_client, finding_groups_fixture):
"""Test resource fields (uid, name, service, region, type) have valid values."""
response = authenticated_client.get(
@@ -17240,3 +17271,111 @@ class TestFindingGroupViewSet:
attrs = item["attributes"]
assert "finding_id" in attrs
assert attrs["finding_id"] in rds_finding_ids
+
+ def test_latest_resources_picks_scan_by_completed_at_when_overlap(
+ self,
+ authenticated_client,
+ tenants_fixture,
+ providers_fixture,
+ resources_fixture,
+ ):
+ """Overlapping scans on the same provider must resolve to the scan
+ with the latest completed_at, matching the /latest summary path and
+ the daily-summary upsert (keyed on midnight(completed_at)). Picking
+ by inserted_at here produced /resources and /latest reading from
+ different scans and reporting diverging delta/new counts.
+ """
+ tenant = tenants_fixture[0]
+ provider = providers_fixture[0]
+ resource = resources_fixture[0]
+ check_id = "overlap_regression_check"
+
+ t0 = datetime.now(timezone.utc) - timedelta(hours=5)
+ t1 = t0 + timedelta(hours=1)
+ t1_end = t1 + timedelta(minutes=30)
+ t2 = t0 + timedelta(hours=4)
+
+ scan_long = Scan.objects.create(
+ name="long overlap scan",
+ provider=provider,
+ trigger=Scan.TriggerChoices.MANUAL,
+ state=StateChoices.COMPLETED,
+ tenant_id=tenant.id,
+ started_at=t0,
+ completed_at=t2,
+ )
+ scan_short = Scan.objects.create(
+ name="short overlap scan",
+ provider=provider,
+ trigger=Scan.TriggerChoices.MANUAL,
+ state=StateChoices.COMPLETED,
+ tenant_id=tenant.id,
+ started_at=t1,
+ completed_at=t1_end,
+ )
+ # inserted_at is auto_now_add so override with .update() to recreate
+ # the overlap shape: short scan inserted later but completed earlier.
+ Scan.all_objects.filter(pk=scan_long.pk).update(inserted_at=t0)
+ Scan.all_objects.filter(pk=scan_short.pk).update(inserted_at=t1)
+ scan_long.refresh_from_db()
+ scan_short.refresh_from_db()
+
+ assert scan_short.inserted_at > scan_long.inserted_at
+ assert scan_long.completed_at > scan_short.completed_at
+
+ long_finding = Finding.objects.create(
+ tenant_id=tenant.id,
+ uid=f"{check_id}_long",
+ scan=scan_long,
+ delta=None,
+ status=Status.FAIL,
+ status_extended="long scan finding",
+ impact=Severity.high,
+ impact_extended="high",
+ severity=Severity.high,
+ raw_result={"status": Status.FAIL, "severity": Severity.high},
+ check_id=check_id,
+ check_metadata={
+ "CheckId": check_id,
+ "checktitle": "Overlap regression",
+ "Description": "Overlapping scan regression.",
+ },
+ first_seen_at=t0,
+ muted=False,
+ )
+ long_finding.add_resources([resource])
+
+ short_finding = Finding.objects.create(
+ tenant_id=tenant.id,
+ uid=f"{check_id}_short",
+ scan=scan_short,
+ delta="new",
+ status=Status.FAIL,
+ status_extended="short scan finding",
+ impact=Severity.high,
+ impact_extended="high",
+ severity=Severity.high,
+ raw_result={"status": Status.FAIL, "severity": Severity.high},
+ check_id=check_id,
+ check_metadata={
+ "CheckId": check_id,
+ "checktitle": "Overlap regression",
+ "Description": "Overlapping scan regression.",
+ },
+ first_seen_at=t1,
+ muted=False,
+ )
+ short_finding.add_resources([resource])
+
+ response = authenticated_client.get(
+ reverse(
+ "finding-group-latest_resources",
+ kwargs={"check_id": check_id},
+ ),
+ )
+ assert response.status_code == status.HTTP_200_OK
+ data = response.json()["data"]
+ assert len(data) == 1
+ attrs = data[0]["attributes"]
+ assert attrs["finding_id"] == str(long_finding.id)
+ assert attrs["delta"] is None
diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py
index 52ec47f4f5..51cb95fbff 100644
--- a/api/src/backend/api/v1/serializers.py
+++ b/api/src/backend/api/v1/serializers.py
@@ -4225,10 +4225,11 @@ class FindingGroupResourceSerializer(BaseSerializerV1):
Serializer for Finding Group Resources - resources within a finding group.
Returns individual resources with their current status, severity,
- and timing information.
+ and timing information. Orphan findings (without any resource) expose the
+ finding id as `id` so the row stays identifiable in the UI.
"""
- id = serializers.UUIDField(source="resource_id")
+ id = serializers.UUIDField(source="row_id")
resource = serializers.SerializerMethodField()
provider = serializers.SerializerMethodField()
finding_id = serializers.UUIDField()
diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py
index 0d085349cc..5a2c790655 100644
--- a/api/src/backend/api/v1/views.py
+++ b/api/src/backend/api/v1/views.py
@@ -35,11 +35,13 @@ from django.db.models import (
CharField,
Count,
DecimalField,
+ Exists,
ExpressionWrapper,
F,
IntegerField,
Max,
Min,
+ OuterRef,
Prefetch,
Q,
QuerySet,
@@ -415,7 +417,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
- spectacular_settings.VERSION = "1.25.0"
+ spectacular_settings.VERSION = "1.26.0"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
@@ -7125,17 +7127,16 @@ class FindingGroupViewSet(BaseRLSViewSet):
output_field=IntegerField(),
)
- # `pass_count`, `fail_count` and `manual_count` count *every* finding
- # for the check (muted or not) so the aggregated `status` reflects the
- # underlying check outcome regardless of mute state. Whether the group
- # is actionable is signalled by the orthogonal `muted` flag below.
+ # `pass_count`, `fail_count` and `manual_count` only count non-muted
+ # findings. Muted findings are tracked separately via the
+ # `*_muted_count` fields.
return (
queryset.values("check_id")
.annotate(
severity_order=Max(severity_case),
- pass_count=Count("id", filter=Q(status="PASS")),
- fail_count=Count("id", filter=Q(status="FAIL")),
- manual_count=Count("id", filter=Q(status="MANUAL")),
+ pass_count=Count("id", filter=Q(status="PASS", muted=False)),
+ fail_count=Count("id", filter=Q(status="FAIL", muted=False)),
+ manual_count=Count("id", filter=Q(status="MANUAL", muted=False)),
pass_muted_count=Count("id", filter=Q(status="PASS", muted=True)),
fail_muted_count=Count("id", filter=Q(status="FAIL", muted=True)),
manual_muted_count=Count("id", filter=Q(status="MANUAL", muted=True)),
@@ -7280,12 +7281,14 @@ class FindingGroupViewSet(BaseRLSViewSet):
# finding-level aggregation path.
row.pop("nonmuted_count", None)
- # Compute aggregated status. Counts are inclusive of muted findings,
- # so the underlying check outcome surfaces even when the group is
- # fully muted.
- if row.get("fail_count", 0) > 0:
+ # Compute aggregated status from non-muted counts first, then
+ # fall back to muted counts so fully-muted groups still reflect
+ # the underlying check outcome.
+ total_fail = row.get("fail_count", 0) + row.get("fail_muted_count", 0)
+ total_pass = row.get("pass_count", 0) + row.get("pass_muted_count", 0)
+ if total_fail > 0:
row["status"] = "FAIL"
- elif row.get("pass_count", 0) > 0:
+ elif total_pass > 0:
row["status"] = "PASS"
else:
row["status"] = "MANUAL"
@@ -7385,9 +7388,12 @@ class FindingGroupViewSet(BaseRLSViewSet):
if computed_params.get("status") or computed_params.getlist("status__in"):
queryset = queryset.annotate(
+ total_fail=F("fail_count") + F("fail_muted_count"),
+ total_pass=F("pass_count") + F("pass_muted_count"),
+ ).annotate(
aggregated_status=Case(
- When(fail_count__gt=0, then=Value("FAIL")),
- When(pass_count__gt=0, then=Value("PASS")),
+ When(total_fail__gt=0, then=Value("FAIL")),
+ When(total_pass__gt=0, then=Value("PASS")),
default=Value("MANUAL"),
output_field=CharField(),
)
@@ -7407,6 +7413,25 @@ class FindingGroupViewSet(BaseRLSViewSet):
return filterset.qs
+ def _resolve_finding_ids(self, filtered_queryset):
+ """
+ Materialize and request-cache the finding_ids list used to anchor
+ RFM lookups.
+
+ Turning `finding_id__in=Subquery(findings_qs)` into `finding_id__in=
+ [uuid, ...]` nudges PostgreSQL out of a Merge Semi Join that ends up
+ reading hundreds of thousands of RFM index entries just to post-
+ filter tenant_id. Caching on the ViewSet instance (one instance per
+ request) avoids duplicating the findings round-trip when several
+ helpers build different RFM querysets from the same filtered set.
+ """
+ cached = getattr(self, "_finding_ids_cache", None)
+ if cached is not None and cached[0] is filtered_queryset:
+ return cached[1]
+ finding_ids = list(filtered_queryset.order_by().values_list("id", flat=True))
+ self._finding_ids_cache = (filtered_queryset, finding_ids)
+ return finding_ids
+
def _build_resource_mapping_queryset(
self, filtered_queryset, resource_ids=None, tenant_id: str | None = None
):
@@ -7416,10 +7441,10 @@ class FindingGroupViewSet(BaseRLSViewSet):
Starting from ResourceFindingMapping avoids scanning all mappings
before applying check_id/date filters on findings.
"""
- finding_ids = filtered_queryset.order_by().values("id")
+ finding_ids = self._resolve_finding_ids(filtered_queryset)
mapping_queryset = ResourceFindingMapping.objects.filter(
- finding_id__in=Subquery(finding_ids)
+ finding_id__in=finding_ids
)
if tenant_id:
mapping_queryset = mapping_queryset.filter(tenant_id=tenant_id)
@@ -7578,6 +7603,53 @@ class FindingGroupViewSet(BaseRLSViewSet):
.order_by(*ordering)
)
+ def _orphan_findings_queryset(self, filtered_queryset, finding_ids=None):
+ """Findings in the filtered set with no ResourceFindingMapping entries."""
+ orphan_qs = filtered_queryset.filter(
+ ~Exists(ResourceFindingMapping.objects.filter(finding_id=OuterRef("pk")))
+ )
+ if finding_ids is not None:
+ orphan_qs = orphan_qs.filter(id__in=finding_ids)
+ return orphan_qs
+
+ def _has_orphan_findings(self, filtered_queryset) -> bool:
+ """Return True if any finding in the filtered set has no resource mapping."""
+ return self._orphan_findings_queryset(filtered_queryset).exists()
+
+ def _orphan_aggregation_values(self, orphan_queryset):
+ """Raw rows for orphan findings; resource payload synthesized from metadata.
+
+ check_metadata is stored with lowercase keys (see
+ `prowler.lib.outputs.finding.Finding.get_metadata`) and
+ `Finding.resource_groups` is already denormalized at ingest time.
+ """
+ return orphan_queryset.annotate(
+ _provider_type=F("scan__provider__provider"),
+ _provider_uid=F("scan__provider__uid"),
+ _provider_alias=F("scan__provider__alias"),
+ _svc=KeyTextTransform("servicename", "check_metadata"),
+ _region=KeyTextTransform("region", "check_metadata"),
+ _rtype=KeyTextTransform("resourcetype", "check_metadata"),
+ _rgroup=F("resource_groups"),
+ ).values(
+ "id",
+ "uid",
+ "status",
+ "severity",
+ "delta",
+ "muted",
+ "muted_reason",
+ "first_seen_at",
+ "inserted_at",
+ "_provider_type",
+ "_provider_uid",
+ "_provider_alias",
+ "_svc",
+ "_region",
+ "_rtype",
+ "_rgroup",
+ )
+
def _post_process_resources(self, resource_data):
"""Convert resource aggregation rows to API output."""
results = []
@@ -7599,9 +7671,13 @@ class FindingGroupViewSet(BaseRLSViewSet):
else:
delta = None
+ resource_id = row["resource_id"]
+ finding_id = str(row["finding_id"]) if row.get("finding_id") else None
+
results.append(
{
- "resource_id": row["resource_id"],
+ "row_id": resource_id,
+ "resource_id": resource_id,
"resource_uid": row["resource_uid"],
"resource_name": row["resource_name"],
"resource_service": row["resource_service"],
@@ -7620,9 +7696,46 @@ class FindingGroupViewSet(BaseRLSViewSet):
"muted": bool(row.get("muted", False)),
"muted_reason": row.get("muted_reason"),
"resource_group": row.get("resource_group", ""),
- "finding_id": (
- str(row["finding_id"]) if row.get("finding_id") else None
- ),
+ "finding_id": finding_id,
+ }
+ )
+
+ return results
+
+ def _post_process_orphans(self, orphan_rows):
+ """Convert orphan finding rows into the same API shape as mapping rows."""
+ results = []
+ for row in orphan_rows:
+ status_val = row["status"]
+ status = status_val if status_val in ("FAIL", "PASS") else "MANUAL"
+
+ muted = bool(row["muted"])
+ delta_val = row.get("delta")
+ delta = delta_val if delta_val in ("new", "changed") and not muted else None
+
+ finding_id = str(row["id"])
+
+ results.append(
+ {
+ "row_id": finding_id,
+ "resource_id": None,
+ "resource_uid": row["uid"],
+ "resource_name": row["uid"],
+ "resource_service": row["_svc"] or "",
+ "resource_region": row["_region"] or "",
+ "resource_type": row["_rtype"] or "",
+ "provider_type": row["_provider_type"],
+ "provider_uid": row["_provider_uid"],
+ "provider_alias": row["_provider_alias"],
+ "status": status,
+ "severity": row["severity"],
+ "delta": delta,
+ "first_seen_at": row["first_seen_at"],
+ "last_seen_at": row["inserted_at"],
+ "muted": muted,
+ "muted_reason": row.get("muted_reason"),
+ "resource_group": row["_rgroup"] or "",
+ "finding_id": finding_id,
}
)
@@ -7683,16 +7796,14 @@ class FindingGroupViewSet(BaseRLSViewSet):
sort_param, self._FINDING_GROUP_SORT_MAP
)
if ordering:
- # status_order is annotated on demand so groups can be sorted by
- # their aggregated status (FAIL > PASS > MANUAL), mirroring the
- # priority used in _post_process_aggregation. Counts are
- # inclusive of muted findings, so the underlying check outcome
- # surfaces even for fully muted groups.
if any(field.lstrip("-") == "status_order" for field in ordering):
aggregated_queryset = aggregated_queryset.annotate(
+ total_fail_for_sort=F("fail_count") + F("fail_muted_count"),
+ total_pass_for_sort=F("pass_count") + F("pass_muted_count"),
+ ).annotate(
status_order=Case(
- When(fail_count__gt=0, then=Value(3)),
- When(pass_count__gt=0, then=Value(2)),
+ When(total_fail_for_sort__gt=0, then=Value(3)),
+ When(total_pass_for_sort__gt=0, then=Value(2)),
default=Value(1),
output_field=IntegerField(),
)
@@ -7731,41 +7842,65 @@ class FindingGroupViewSet(BaseRLSViewSet):
def _paginated_resource_response(
self, request, filtered_queryset, resource_ids, tenant_id
):
- """Paginate and return resources.
+ """Paginate and return resources, appending orphan findings when present.
- Without sort: paginate lightweight resource IDs first, aggregate only the page.
- With sort: build a lightweight ordering subquery (resource_id + sort keys),
- paginate that, then aggregate full details only for the page.
+ Hot path (no orphans, or resource filter applied): resources come from
+ ResourceFindingMapping aggregation. Untouched pre-existing behaviour.
+
+ Orphan fallback: findings without a mapping (e.g. IaC) are appended
+ after mapping rows as synthesised resource-like rows so they remain
+ visible in the UI without paying the aggregation cost on the hot path.
"""
sort_param = request.query_params.get("sort")
-
+ ordering = None
if sort_param:
- ordering = self._validate_sort_fields(sort_param, self._RESOURCE_SORT_MAP)
- if ordering:
- if "resource_id" not in {field.lstrip("-") for field in ordering}:
- ordering.append("resource_id")
+ validated = self._validate_sort_fields(sort_param, self._RESOURCE_SORT_MAP)
+ ordering = validated if validated else None
- # Phase 1: lightweight aggregation with only sort keys, paginate
- ordering_qs = self._build_resource_ordering_queryset(
- filtered_queryset,
- resource_ids=resource_ids,
- tenant_id=tenant_id,
- ordering=ordering,
- )
- page = self.paginate_queryset(ordering_qs)
- if page is not None:
- page_ids = [row["resource_id"] for row in page]
- resource_data = self._build_resource_aggregation(
- filtered_queryset, resource_ids=page_ids, tenant_id=tenant_id
- )
- # Re-sort to match the page ordering
- id_order = {rid: idx for idx, rid in enumerate(page_ids)}
- results = self._post_process_resources(resource_data)
- results.sort(key=lambda r: id_order.get(r["resource_id"], 0))
- serializer = FindingGroupResourceSerializer(results, many=True)
- return self.get_paginated_response(serializer.data)
+ # Resource filters can only match findings with resources; skip orphan
+ # detection entirely when they are present.
+ if resource_ids is not None:
+ return self._mapping_paginated_response(
+ request, filtered_queryset, resource_ids, tenant_id, ordering
+ )
- page_ids = [row["resource_id"] for row in ordering_qs]
+ # Serve the mapping response directly and piggyback on the paginator
+ # count to detect orphan-only groups, instead of paying a separate
+ # has_mappings.exists() semi-join over ResourceFindingMapping on
+ # every non-IaC request. TODO: once the ephemeral resources strategy
+ # is decided, mixed groups should route to _combined_paginated_response.
+ response = self._mapping_paginated_response(
+ request, filtered_queryset, resource_ids, tenant_id, ordering
+ )
+
+ page = getattr(self.paginator, "page", None)
+ mapping_total = page.paginator.count if page is not None else None
+ if mapping_total == 0:
+ # Pure orphan group (e.g. IaC): synthesize resource-like rows.
+ return self._combined_paginated_response(
+ request, filtered_queryset, tenant_id, ordering
+ )
+
+ return response
+
+ def _mapping_paginated_response(
+ self, request, filtered_queryset, resource_ids, tenant_id, ordering
+ ):
+ """Mapping-only paginated response (original fast path)."""
+ if ordering:
+ if "resource_id" not in {field.lstrip("-") for field in ordering}:
+ ordering.append("resource_id")
+
+ # Phase 1: lightweight aggregation with only sort keys, paginate
+ ordering_qs = self._build_resource_ordering_queryset(
+ filtered_queryset,
+ resource_ids=resource_ids,
+ tenant_id=tenant_id,
+ ordering=ordering,
+ )
+ page = self.paginate_queryset(ordering_qs)
+ if page is not None:
+ page_ids = [row["resource_id"] for row in page]
resource_data = self._build_resource_aggregation(
filtered_queryset, resource_ids=page_ids, tenant_id=tenant_id
)
@@ -7773,10 +7908,18 @@ class FindingGroupViewSet(BaseRLSViewSet):
results = self._post_process_resources(resource_data)
results.sort(key=lambda r: id_order.get(r["resource_id"], 0))
serializer = FindingGroupResourceSerializer(results, many=True)
- return Response(serializer.data)
+ return self.get_paginated_response(serializer.data)
+
+ page_ids = [row["resource_id"] for row in ordering_qs]
+ resource_data = self._build_resource_aggregation(
+ filtered_queryset, resource_ids=page_ids, tenant_id=tenant_id
+ )
+ id_order = {rid: idx for idx, rid in enumerate(page_ids)}
+ results = self._post_process_resources(resource_data)
+ results.sort(key=lambda r: id_order.get(r["resource_id"], 0))
+ serializer = FindingGroupResourceSerializer(results, many=True)
+ return Response(serializer.data)
- # No sort (or only empty sort fragments): paginate lightweight resource IDs
- # first, aggregate only the page.
mapping_qs = self._build_resource_mapping_queryset(
filtered_queryset, resource_ids=resource_ids, tenant_id=tenant_id
)
@@ -7804,6 +7947,95 @@ class FindingGroupViewSet(BaseRLSViewSet):
serializer = FindingGroupResourceSerializer(results, many=True)
return Response(serializer.data)
+ def _combined_paginated_response(
+ self, request, filtered_queryset, tenant_id, ordering
+ ):
+ """Mapping rows + orphan findings appended at end.
+
+ Orphans sit after mapping rows regardless of sort. This keeps the
+ mapping-only code path intact for checks that have no orphans (the
+ common case) and avoids paying UNION/coalesce costs there.
+ """
+ mapping_qs = self._build_resource_mapping_queryset(
+ filtered_queryset, resource_ids=None, tenant_id=tenant_id
+ )
+ mapping_count = mapping_qs.values("resource_id").distinct().count()
+
+ orphan_ids = list(
+ self._orphan_findings_queryset(filtered_queryset)
+ .order_by("id")
+ .values_list("id", flat=True)
+ )
+ orphan_count = len(orphan_ids)
+ total = mapping_count + orphan_count
+
+ # Paginate a simple [0..total) index sequence so DRF produces proper
+ # links/meta; then slice mapping / orphan sources accordingly.
+ page = self.paginate_queryset(range(total))
+ page_indices = list(page) if page is not None else list(range(total))
+
+ mapping_indices = [i for i in page_indices if i < mapping_count]
+ orphan_positions = [
+ i - mapping_count for i in page_indices if i >= mapping_count
+ ]
+
+ mapping_results = []
+ if mapping_indices:
+ start = mapping_indices[0]
+ stop = mapping_indices[-1] + 1
+ if ordering:
+ ordering_fields = list(ordering)
+ if "resource_id" not in {
+ field.lstrip("-") for field in ordering_fields
+ }:
+ ordering_fields.append("resource_id")
+ ordered_qs = self._build_resource_ordering_queryset(
+ filtered_queryset,
+ resource_ids=None,
+ tenant_id=tenant_id,
+ ordering=ordering_fields,
+ )
+ slice_rids = [row["resource_id"] for row in ordered_qs[start:stop]]
+ else:
+ slice_rids = list(
+ mapping_qs.values_list("resource_id", flat=True)
+ .distinct()
+ .order_by("resource_id")[start:stop]
+ )
+ if slice_rids:
+ resource_data = self._build_resource_aggregation(
+ filtered_queryset,
+ resource_ids=slice_rids,
+ tenant_id=tenant_id,
+ )
+ rows_by_rid = {row["resource_id"]: row for row in resource_data}
+ ordered_rows = [
+ rows_by_rid[rid] for rid in slice_rids if rid in rows_by_rid
+ ]
+ mapping_results = self._post_process_resources(ordered_rows)
+
+ orphan_results = []
+ if orphan_positions:
+ slice_fids = [orphan_ids[pos] for pos in orphan_positions]
+ raw_rows = list(
+ self._orphan_aggregation_values(
+ self._orphan_findings_queryset(
+ filtered_queryset, finding_ids=slice_fids
+ )
+ )
+ )
+ rows_by_fid = {row["id"]: row for row in raw_rows}
+ ordered_rows = [
+ rows_by_fid[fid] for fid in slice_fids if fid in rows_by_fid
+ ]
+ orphan_results = self._post_process_orphans(ordered_rows)
+
+ results = mapping_results + orphan_results
+ serializer = FindingGroupResourceSerializer(results, many=True)
+ if page is not None:
+ return self.get_paginated_response(serializer.data)
+ return Response(serializer.data)
+
def list(self, request, *args, **kwargs):
"""
List finding groups with aggregation and filtering.
@@ -7933,10 +8165,13 @@ class FindingGroupViewSet(BaseRLSViewSet):
tenant_id = request.tenant_id
queryset = self._get_finding_queryset()
- # Get latest completed scan for each provider
+ # Order by -completed_at (matching the /latest summary path and the
+ # daily summary upsert keyed on midnight(completed_at)) so that
+ # overlapping scans do not make /resources and /latest read from
+ # different scans and report diverging counts.
latest_scan_ids = (
Scan.objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
- .order_by("provider_id", "-inserted_at")
+ .order_by("provider_id", "-completed_at", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
)
diff --git a/api/src/backend/config/celery.py b/api/src/backend/config/celery.py
index aaa1b1c386..c46c8a426c 100644
--- a/api/src/backend/config/celery.py
+++ b/api/src/backend/config/celery.py
@@ -17,8 +17,10 @@ celery_app.config_from_object("django.conf:settings", namespace="CELERY")
celery_app.conf.update(result_extended=True, result_expires=None)
celery_app.conf.broker_transport_options = {
- "visibility_timeout": BROKER_VISIBILITY_TIMEOUT
+ "visibility_timeout": BROKER_VISIBILITY_TIMEOUT,
+ "queue_order_strategy": "priority",
}
+celery_app.conf.task_default_priority = 6
celery_app.conf.result_backend_transport_options = {
"visibility_timeout": BROKER_VISIBILITY_TIMEOUT
}
diff --git a/api/src/backend/tasks/jobs/attack_paths/aws.py b/api/src/backend/tasks/jobs/attack_paths/aws.py
index e7f26b5173..7248cc39e8 100644
--- a/api/src/backend/tasks/jobs/attack_paths/aws.py
+++ b/api/src/backend/tasks/jobs/attack_paths/aws.py
@@ -1,6 +1,8 @@
# Portions of this file are based on code from the Cartography project
# (https://github.com/cartography-cncf/cartography), which is licensed under the Apache 2.0 License.
+import time
+
from typing import Any
import aioboto3
@@ -33,7 +35,7 @@ def start_aws_ingestion(
For the scan progress updates:
- The caller of this function (`tasks.jobs.attack_paths.scan.run`) has set it to 2.
- - When the control returns to the caller, it will be set to 95.
+ - When the control returns to the caller, it will be set to 93.
"""
# Initialize variables common to all jobs
@@ -89,34 +91,50 @@ def start_aws_ingestion(
logger.info(
f"Syncing function permission_relationships for AWS account {prowler_api_provider.uid}"
)
+ t0 = time.perf_counter()
cartography_aws.RESOURCE_FUNCTIONS["permission_relationships"](**sync_args)
+ logger.info(
+ f"Synced function permission_relationships for AWS account {prowler_api_provider.uid} in {time.perf_counter() - t0:.3f}s"
+ )
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 88)
if "resourcegroupstaggingapi" in requested_syncs:
logger.info(
f"Syncing function resourcegroupstaggingapi for AWS account {prowler_api_provider.uid}"
)
+ t0 = time.perf_counter()
cartography_aws.RESOURCE_FUNCTIONS["resourcegroupstaggingapi"](**sync_args)
+ logger.info(
+ f"Synced function resourcegroupstaggingapi for AWS account {prowler_api_provider.uid} in {time.perf_counter() - t0:.3f}s"
+ )
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 89)
logger.info(
f"Syncing ec2_iaminstanceprofile scoped analysis for AWS account {prowler_api_provider.uid}"
)
+ t0 = time.perf_counter()
cartography_aws.run_scoped_analysis_job(
"aws_ec2_iaminstanceprofile.json",
neo4j_session,
common_job_parameters,
)
+ logger.info(
+ f"Synced ec2_iaminstanceprofile scoped analysis for AWS account {prowler_api_provider.uid} in {time.perf_counter() - t0:.3f}s"
+ )
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 90)
logger.info(
f"Syncing lambda_ecr analysis for AWS account {prowler_api_provider.uid}"
)
+ t0 = time.perf_counter()
cartography_aws.run_analysis_job(
"aws_lambda_ecr.json",
neo4j_session,
common_job_parameters,
)
+ logger.info(
+ f"Synced lambda_ecr analysis for AWS account {prowler_api_provider.uid} in {time.perf_counter() - t0:.3f}s"
+ )
if all(
s in requested_syncs
@@ -125,25 +143,34 @@ def start_aws_ingestion(
logger.info(
f"Syncing lb_container_exposure scoped analysis for AWS account {prowler_api_provider.uid}"
)
+ t0 = time.perf_counter()
cartography_aws.run_scoped_analysis_job(
"aws_lb_container_exposure.json",
neo4j_session,
common_job_parameters,
)
+ logger.info(
+ f"Synced lb_container_exposure scoped analysis for AWS account {prowler_api_provider.uid} in {time.perf_counter() - t0:.3f}s"
+ )
if all(s in requested_syncs for s in ["ec2:network_acls", "ec2:load_balancer_v2"]):
logger.info(
f"Syncing lb_nacl_direct scoped analysis for AWS account {prowler_api_provider.uid}"
)
+ t0 = time.perf_counter()
cartography_aws.run_scoped_analysis_job(
"aws_lb_nacl_direct.json",
neo4j_session,
common_job_parameters,
)
+ logger.info(
+ f"Synced lb_nacl_direct scoped analysis for AWS account {prowler_api_provider.uid} in {time.perf_counter() - t0:.3f}s"
+ )
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 91)
logger.info(f"Syncing metadata for AWS account {prowler_api_provider.uid}")
+ t0 = time.perf_counter()
cartography_aws.merge_module_sync_metadata(
neo4j_session,
group_type="AWSAccount",
@@ -152,24 +179,23 @@ def start_aws_ingestion(
update_tag=cartography_config.update_tag,
stat_handler=cartography_aws.stat_handler,
)
+ logger.info(
+ f"Synced metadata for AWS account {prowler_api_provider.uid} in {time.perf_counter() - t0:.3f}s"
+ )
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 92)
# Removing the added extra field
del common_job_parameters["AWS_ID"]
- logger.info(f"Syncing cleanup_job for AWS account {prowler_api_provider.uid}")
- cartography_aws.run_cleanup_job(
- "aws_post_ingestion_principals_cleanup.json",
- neo4j_session,
- common_job_parameters,
- )
- db_utils.update_attack_paths_scan_progress(attack_paths_scan, 93)
-
logger.info(f"Syncing analysis for AWS account {prowler_api_provider.uid}")
+ t0 = time.perf_counter()
cartography_aws._perform_aws_analysis(
requested_syncs, neo4j_session, common_job_parameters
)
- db_utils.update_attack_paths_scan_progress(attack_paths_scan, 94)
+ logger.info(
+ f"Synced analysis for AWS account {prowler_api_provider.uid} in {time.perf_counter() - t0:.3f}s"
+ )
+ db_utils.update_attack_paths_scan_progress(attack_paths_scan, 93)
return failed_syncs
@@ -234,6 +260,8 @@ def sync_aws_account(
)
try:
+ func_t0 = time.perf_counter()
+
# `ecr:image_layers` uses `aioboto3_session` instead of `boto3_session`
if func_name == "ecr:image_layers":
cartography_aws.RESOURCE_FUNCTIONS[func_name](
@@ -257,7 +285,15 @@ def sync_aws_account(
else:
cartography_aws.RESOURCE_FUNCTIONS[func_name](**sync_args)
+ logger.info(
+ f"Synced function {func_name} for AWS account {prowler_api_provider.uid} in {time.perf_counter() - func_t0:.3f}s"
+ )
+
except Exception as e:
+ logger.info(
+ f"Synced function {func_name} for AWS account {prowler_api_provider.uid} in {time.perf_counter() - func_t0:.3f}s (FAILED)"
+ )
+
exception_message = utils.stringify_exception(
e, f"Exception for AWS sync function: {func_name}"
)
diff --git a/api/src/backend/tasks/jobs/attack_paths/config.py b/api/src/backend/tasks/jobs/attack_paths/config.py
index 76dbdc1dc5..5f5c523ceb 100644
--- a/api/src/backend/tasks/jobs/attack_paths/config.py
+++ b/api/src/backend/tasks/jobs/attack_paths/config.py
@@ -8,9 +8,9 @@ from tasks.jobs.attack_paths import aws
# Batch size for Neo4j write operations (resource labeling, cleanup)
BATCH_SIZE = env.int("ATTACK_PATHS_BATCH_SIZE", 1000)
# Batch size for Postgres findings fetch (keyset pagination page size)
-FINDINGS_BATCH_SIZE = env.int("ATTACK_PATHS_FINDINGS_BATCH_SIZE", 500)
+FINDINGS_BATCH_SIZE = env.int("ATTACK_PATHS_FINDINGS_BATCH_SIZE", 1000)
# Batch size for temp-to-tenant graph sync (nodes and relationships per cursor page)
-SYNC_BATCH_SIZE = env.int("ATTACK_PATHS_SYNC_BATCH_SIZE", 250)
+SYNC_BATCH_SIZE = env.int("ATTACK_PATHS_SYNC_BATCH_SIZE", 1000)
# Neo4j internal labels (Prowler-specific, not provider-specific)
# - `Internet`: Singleton node representing external internet access for exposed-resource queries
diff --git a/api/src/backend/tasks/jobs/attack_paths/findings.py b/api/src/backend/tasks/jobs/attack_paths/findings.py
index 56df42bf9d..0b2ecb4c45 100644
--- a/api/src/backend/tasks/jobs/attack_paths/findings.py
+++ b/api/src/backend/tasks/jobs/attack_paths/findings.py
@@ -12,6 +12,7 @@ from typing import Any, Generator
from uuid import UUID
import neo4j
+
from cartography.config import Config as CartographyConfig
from celery.utils.log import get_task_logger
from tasks.jobs.attack_paths.config import (
@@ -86,17 +87,21 @@ def analysis(
prowler_api_provider: Provider,
scan_id: str,
config: CartographyConfig,
-) -> None:
+) -> tuple[int, int]:
"""
Main entry point for Prowler findings analysis.
Adds resource labels and loads findings.
+ Returns (labeled_nodes, findings_loaded).
"""
- add_resource_label(
+ total_labeled = add_resource_label(
neo4j_session, prowler_api_provider.provider, str(prowler_api_provider.uid)
)
findings_data = stream_findings_with_resources(prowler_api_provider, scan_id)
- load_findings(neo4j_session, findings_data, prowler_api_provider, config)
+ total_loaded = load_findings(
+ neo4j_session, findings_data, prowler_api_provider, config
+ )
+ return total_labeled, total_loaded
def add_resource_label(
@@ -146,12 +151,11 @@ def load_findings(
findings_batches: Generator[list[dict[str, Any]], None, None],
prowler_api_provider: Provider,
config: CartographyConfig,
-) -> None:
+) -> int:
"""Load Prowler findings into the graph, linking them to resources."""
query = render_cypher_template(
INSERT_FINDING_TEMPLATE,
{
- "__ROOT_NODE_LABEL__": get_root_node_label(prowler_api_provider.provider),
"__NODE_UID_FIELD__": get_node_uid_field(prowler_api_provider.provider),
"__RESOURCE_LABEL__": get_provider_resource_label(
prowler_api_provider.provider
@@ -160,7 +164,6 @@ def load_findings(
)
parameters = {
- "provider_uid": str(prowler_api_provider.uid),
"last_updated": config.update_tag,
"prowler_version": ProwlerConfig.prowler_version,
}
@@ -178,6 +181,7 @@ def load_findings(
neo4j_session.run(query, parameters)
logger.info(f"Finished loading {total_records} records in {batch_num} batches")
+ return total_records
# Findings Streaming (Generator-based)
@@ -248,7 +252,9 @@ def _fetch_findings_batch(
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
# Use `all_objects` to get `Findings` even on soft-deleted `Providers`
# But even the provider is already validated as active in this context
- qs = FindingModel.all_objects.filter(scan_id=scan_id).order_by("id")
+ qs = FindingModel.all_objects.filter(
+ tenant_id=tenant_id, scan_id=scan_id
+ ).order_by("id")
if after_id is not None:
qs = qs.filter(id__gt=after_id)
diff --git a/api/src/backend/tasks/jobs/attack_paths/queries.py b/api/src/backend/tasks/jobs/attack_paths/queries.py
index ab13150b56..26ffa32f92 100644
--- a/api/src/backend/tasks/jobs/attack_paths/queries.py
+++ b/api/src/backend/tasks/jobs/attack_paths/queries.py
@@ -32,17 +32,14 @@ ADD_RESOURCE_LABEL_TEMPLATE = """
"""
INSERT_FINDING_TEMPLATE = f"""
- MATCH (account:__ROOT_NODE_LABEL__ {{id: $provider_uid}})
UNWIND $findings_data AS finding_data
- OPTIONAL MATCH (account)-->(resource_by_uid:__RESOURCE_LABEL__)
- WHERE resource_by_uid.__NODE_UID_FIELD__ = finding_data.resource_uid
- WITH account, finding_data, resource_by_uid
+ OPTIONAL MATCH (resource_by_uid:__RESOURCE_LABEL__ {{__NODE_UID_FIELD__: finding_data.resource_uid}})
+ WITH finding_data, resource_by_uid
- OPTIONAL MATCH (account)-->(resource_by_id:__RESOURCE_LABEL__)
+ OPTIONAL MATCH (resource_by_id:__RESOURCE_LABEL__ {{id: finding_data.resource_uid}})
WHERE resource_by_uid IS NULL
- AND resource_by_id.id = finding_data.resource_uid
- WITH account, finding_data, COALESCE(resource_by_uid, resource_by_id) AS resource
+ WITH finding_data, COALESCE(resource_by_uid, resource_by_id) AS resource
WHERE resource IS NOT NULL
MERGE (finding:{PROWLER_FINDING_LABEL} {{id: finding_data.id}})
diff --git a/api/src/backend/tasks/jobs/attack_paths/scan.py b/api/src/backend/tasks/jobs/attack_paths/scan.py
index a53a6a530f..382c231eeb 100644
--- a/api/src/backend/tasks/jobs/attack_paths/scan.py
+++ b/api/src/backend/tasks/jobs/attack_paths/scan.py
@@ -55,6 +55,7 @@ exception propagates to Celery.
import logging
import time
+
from typing import Any
from cartography.config import Config as CartographyConfig
@@ -144,6 +145,12 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
attack_paths_scan, task_id, tenant_cartography_config
)
+ scan_t0 = time.perf_counter()
+ logger.info(
+ f"Starting Attack Paths scan ({attack_paths_scan.id}) for "
+ f"{prowler_api_provider.provider.upper()} provider {prowler_api_provider.id}"
+ )
+
subgraph_dropped = False
sync_completed = False
provider_gated = False
@@ -169,6 +176,7 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 2)
# The real scan, where iterates over cloud services
+ t0 = time.perf_counter()
ingestion_exceptions = utils.call_within_event_loop(
cartography_ingestion_function,
tmp_neo4j_session,
@@ -177,19 +185,23 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
prowler_sdk_provider,
attack_paths_scan,
)
+ logger.info(
+ f"Cartography ingestion completed in {time.perf_counter() - t0:.3f}s "
+ f"(failed_syncs={len(ingestion_exceptions)})"
+ )
# Post-processing: Just keeping it to be more Cartography compliant
logger.info(
f"Syncing Cartography ontology for AWS account {prowler_api_provider.uid}"
)
cartography_ontology.run(tmp_neo4j_session, tmp_cartography_config)
- db_utils.update_attack_paths_scan_progress(attack_paths_scan, 95)
+ db_utils.update_attack_paths_scan_progress(attack_paths_scan, 94)
logger.info(
f"Syncing Cartography analysis for AWS account {prowler_api_provider.uid}"
)
cartography_analysis.run(tmp_neo4j_session, tmp_cartography_config)
- db_utils.update_attack_paths_scan_progress(attack_paths_scan, 96)
+ db_utils.update_attack_paths_scan_progress(attack_paths_scan, 95)
# Creating Internet node and CAN_ACCESS relationships
logger.info(
@@ -198,14 +210,20 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
internet.analysis(
tmp_neo4j_session, prowler_api_provider, tmp_cartography_config
)
+ db_utils.update_attack_paths_scan_progress(attack_paths_scan, 96)
# Adding Prowler Finding nodes and relationships
logger.info(
f"Syncing Prowler analysis for AWS account {prowler_api_provider.uid}"
)
- findings.analysis(
+ t0 = time.perf_counter()
+ labeled_nodes, findings_loaded = findings.analysis(
tmp_neo4j_session, prowler_api_provider, scan_id, tmp_cartography_config
)
+ logger.info(
+ f"Prowler analysis completed in {time.perf_counter() - t0:.3f}s "
+ f"(findings={findings_loaded}, labeled_nodes={labeled_nodes})"
+ )
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 97)
logger.info(
@@ -227,22 +245,33 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
logger.info(f"Deleting existing provider graph in {tenant_database_name}")
db_utils.set_provider_graph_data_ready(attack_paths_scan, False)
provider_gated = True
- graph_database.drop_subgraph(
+
+ t0 = time.perf_counter()
+ deleted_nodes = graph_database.drop_subgraph(
database=tenant_database_name,
provider_id=str(prowler_api_provider.id),
)
+ logger.info(
+ f"Deleted existing provider graph in {time.perf_counter() - t0:.3f}s "
+ f"(deleted_nodes={deleted_nodes})"
+ )
subgraph_dropped = True
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 98)
logger.info(
f"Syncing graph from {tmp_database_name} into {tenant_database_name}"
)
- sync.sync_graph(
+ t0 = time.perf_counter()
+ sync_result = sync.sync_graph(
source_database=tmp_database_name,
target_database=tenant_database_name,
tenant_id=str(prowler_api_provider.tenant_id),
provider_id=str(prowler_api_provider.id),
)
+ logger.info(
+ f"Synced graph in {time.perf_counter() - t0:.3f}s "
+ f"(nodes={sync_result['nodes']}, relationships={sync_result['relationships']})"
+ )
sync_completed = True
db_utils.set_graph_data_ready(attack_paths_scan, True)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 99)
@@ -250,17 +279,16 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
logger.info(f"Clearing Neo4j cache for database {tenant_database_name}")
graph_database.clear_cache(tenant_database_name)
- logger.info(
- f"Completed Cartography ({attack_paths_scan.id}) for "
- f"{prowler_api_provider.provider.upper()} provider {prowler_api_provider.id}"
- )
-
logger.info(f"Dropping temporary Neo4j database {tmp_database_name}")
graph_database.drop_database(tmp_database_name)
db_utils.finish_attack_paths_scan(
attack_paths_scan, StateChoices.COMPLETED, ingestion_exceptions
)
+ logger.info(
+ f"Attack Paths scan completed in {time.perf_counter() - scan_t0:.3f}s "
+ f"(state=completed, failed_syncs={len(ingestion_exceptions)})"
+ )
return ingestion_exceptions
except Exception as e:
diff --git a/api/src/backend/tasks/jobs/attack_paths/sync.py b/api/src/backend/tasks/jobs/attack_paths/sync.py
index 24ffa6cf48..f720a12e82 100644
--- a/api/src/backend/tasks/jobs/attack_paths/sync.py
+++ b/api/src/backend/tasks/jobs/attack_paths/sync.py
@@ -5,6 +5,8 @@ This module handles syncing graph data from temporary scan databases
to the tenant database, adding provider isolation labels and properties.
"""
+import time
+
from collections import defaultdict
from typing import Any
@@ -81,6 +83,7 @@ def sync_nodes(
Source and target sessions are opened sequentially per batch to avoid
holding two Bolt connections simultaneously for the entire sync duration.
"""
+ t0 = time.perf_counter()
last_id = -1
total_synced = 0
@@ -117,7 +120,7 @@ def sync_nodes(
total_synced += batch_count
logger.info(
- f"Synced {total_synced} nodes from {source_database} to {target_database}"
+ f"Synced {total_synced} nodes from {source_database} to {target_database} in {time.perf_counter() - t0:.3f}s"
)
return total_synced
@@ -136,6 +139,7 @@ def sync_relationships(
Source and target sessions are opened sequentially per batch to avoid
holding two Bolt connections simultaneously for the entire sync duration.
"""
+ t0 = time.perf_counter()
last_id = -1
total_synced = 0
@@ -166,7 +170,7 @@ def sync_relationships(
total_synced += batch_count
logger.info(
- f"Synced {total_synced} relationships from {source_database} to {target_database}"
+ f"Synced {total_synced} relationships from {source_database} to {target_database} in {time.perf_counter() - t0:.3f}s"
)
return total_synced
diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py
index 2c73c97f2f..456fd786cb 100644
--- a/api/src/backend/tasks/jobs/scan.py
+++ b/api/src/backend/tasks/jobs/scan.py
@@ -752,11 +752,19 @@ def _process_finding_micro_batch(
)
if mappings_to_create:
- ResourceFindingMapping.objects.bulk_create(
+ created_mappings = ResourceFindingMapping.objects.bulk_create(
mappings_to_create,
batch_size=SCAN_DB_BATCH_SIZE,
ignore_conflicts=True,
+ unique_fields=["tenant_id", "resource_id", "finding_id"],
)
+ inserted = sum(1 for m in created_mappings if m.pk)
+ if inserted != len(mappings_to_create):
+ logger.error(
+ f"scan {scan_instance.id}: expected "
+ f"{len(mappings_to_create)} ResourceFindingMapping rows, "
+ f"inserted {inserted}. Rolling back micro-batch."
+ )
# Update finding denormalized arrays
findings_to_update = []
@@ -1804,11 +1812,9 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str):
)
# Aggregate findings by check_id for this scan.
- # `pass_count`, `fail_count` and `manual_count` count *every* finding
- # in this group, regardless of mute state, so the aggregated `status`
- # always reflects the underlying check outcome (FAIL > PASS > MANUAL)
- # even when the group is fully muted. The orthogonal `muted` flag is
- # what tells whether the group has any actionable (non-muted) findings.
+ # `pass_count`, `fail_count` and `manual_count` only count non-muted
+ # findings. Muted findings are tracked separately via the
+ # `*_muted_count` fields.
aggregated = (
Finding.objects.filter(
tenant_id=tenant_id,
@@ -1817,9 +1823,9 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str):
.values("check_id")
.annotate(
severity_order=Max(severity_case),
- pass_count=Count("id", filter=Q(status="PASS")),
- fail_count=Count("id", filter=Q(status="FAIL")),
- manual_count=Count("id", filter=Q(status="MANUAL")),
+ pass_count=Count("id", filter=Q(status="PASS", muted=False)),
+ fail_count=Count("id", filter=Q(status="FAIL", muted=False)),
+ manual_count=Count("id", filter=Q(status="MANUAL", muted=False)),
pass_muted_count=Count("id", filter=Q(status="PASS", muted=True)),
fail_muted_count=Count("id", filter=Q(status="FAIL", muted=True)),
manual_muted_count=Count("id", filter=Q(status="MANUAL", muted=True)),
diff --git a/api/src/backend/tasks/tests/test_attack_paths_scan.py b/api/src/backend/tasks/tests/test_attack_paths_scan.py
index 1b1beb11a9..283c0650e1 100644
--- a/api/src/backend/tasks/tests/test_attack_paths_scan.py
+++ b/api/src/backend/tasks/tests/test_attack_paths_scan.py
@@ -38,11 +38,14 @@ class TestAttackPathsRun:
@patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
- @patch("tasks.jobs.attack_paths.scan.sync.sync_graph")
- @patch("tasks.jobs.attack_paths.scan.graph_database.drop_subgraph")
+ @patch(
+ "tasks.jobs.attack_paths.scan.sync.sync_graph",
+ return_value={"nodes": 0, "relationships": 0},
+ )
+ @patch("tasks.jobs.attack_paths.scan.graph_database.drop_subgraph", return_value=0)
@patch("tasks.jobs.attack_paths.scan.indexes.create_sync_indexes")
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
- @patch("tasks.jobs.attack_paths.scan.findings.analysis")
+ @patch("tasks.jobs.attack_paths.scan.findings.analysis", return_value=(0, 0))
@patch("tasks.jobs.attack_paths.scan.indexes.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_ontology.run")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@@ -188,7 +191,7 @@ class TestAttackPathsRun:
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
- @patch("tasks.jobs.attack_paths.scan.findings.analysis")
+ @patch("tasks.jobs.attack_paths.scan.findings.analysis", return_value=(0, 0))
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
@patch("tasks.jobs.attack_paths.scan.indexes.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@@ -287,7 +290,7 @@ class TestAttackPathsRun:
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
- @patch("tasks.jobs.attack_paths.scan.findings.analysis")
+ @patch("tasks.jobs.attack_paths.scan.findings.analysis", return_value=(0, 0))
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
@patch("tasks.jobs.attack_paths.scan.indexes.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@@ -390,7 +393,7 @@ class TestAttackPathsRun:
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
- @patch("tasks.jobs.attack_paths.scan.findings.analysis")
+ @patch("tasks.jobs.attack_paths.scan.findings.analysis", return_value=(0, 0))
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
@patch("tasks.jobs.attack_paths.scan.indexes.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@@ -489,14 +492,17 @@ class TestAttackPathsRun:
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
- @patch("tasks.jobs.attack_paths.scan.sync.sync_graph")
+ @patch(
+ "tasks.jobs.attack_paths.scan.sync.sync_graph",
+ return_value={"nodes": 0, "relationships": 0},
+ )
@patch(
"tasks.jobs.attack_paths.scan.graph_database.drop_subgraph",
side_effect=RuntimeError("drop failed"),
)
@patch("tasks.jobs.attack_paths.scan.indexes.create_sync_indexes")
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
- @patch("tasks.jobs.attack_paths.scan.findings.analysis")
+ @patch("tasks.jobs.attack_paths.scan.findings.analysis", return_value=(0, 0))
@patch("tasks.jobs.attack_paths.scan.indexes.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_ontology.run")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@@ -609,7 +615,7 @@ class TestAttackPathsRun:
@patch("tasks.jobs.attack_paths.scan.graph_database.drop_subgraph")
@patch("tasks.jobs.attack_paths.scan.indexes.create_sync_indexes")
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
- @patch("tasks.jobs.attack_paths.scan.findings.analysis")
+ @patch("tasks.jobs.attack_paths.scan.findings.analysis", return_value=(0, 0))
@patch("tasks.jobs.attack_paths.scan.indexes.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_ontology.run")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@@ -718,11 +724,14 @@ class TestAttackPathsRun:
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
- @patch("tasks.jobs.attack_paths.scan.sync.sync_graph")
+ @patch(
+ "tasks.jobs.attack_paths.scan.sync.sync_graph",
+ return_value={"nodes": 0, "relationships": 0},
+ )
@patch("tasks.jobs.attack_paths.scan.graph_database.drop_subgraph")
@patch("tasks.jobs.attack_paths.scan.indexes.create_sync_indexes")
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
- @patch("tasks.jobs.attack_paths.scan.findings.analysis")
+ @patch("tasks.jobs.attack_paths.scan.findings.analysis", return_value=(0, 0))
@patch("tasks.jobs.attack_paths.scan.indexes.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_ontology.run")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@@ -833,14 +842,17 @@ class TestAttackPathsRun:
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
- @patch("tasks.jobs.attack_paths.scan.sync.sync_graph")
+ @patch(
+ "tasks.jobs.attack_paths.scan.sync.sync_graph",
+ return_value={"nodes": 0, "relationships": 0},
+ )
@patch(
"tasks.jobs.attack_paths.scan.graph_database.drop_subgraph",
side_effect=RuntimeError("drop failed"),
)
@patch("tasks.jobs.attack_paths.scan.indexes.create_sync_indexes")
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
- @patch("tasks.jobs.attack_paths.scan.findings.analysis")
+ @patch("tasks.jobs.attack_paths.scan.findings.analysis", return_value=(0, 0))
@patch("tasks.jobs.attack_paths.scan.indexes.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_ontology.run")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@@ -1274,10 +1286,6 @@ class TestAttackPathsFindingsHelpers:
mock_session = MagicMock()
with (
- patch(
- "tasks.jobs.attack_paths.findings.get_root_node_label",
- return_value="AWSAccount",
- ),
patch(
"tasks.jobs.attack_paths.findings.get_node_uid_field",
return_value="arn",
@@ -1294,7 +1302,6 @@ class TestAttackPathsFindingsHelpers:
assert mock_session.run.call_count == 2
for call_args in mock_session.run.call_args_list:
params = call_args.args[1]
- assert params["provider_uid"] == str(provider.uid)
assert params["last_updated"] == config.update_tag
assert "findings_data" in params
@@ -1673,10 +1680,6 @@ class TestAttackPathsFindingsHelpers:
yield # Make it a generator
with (
- patch(
- "tasks.jobs.attack_paths.findings.get_root_node_label",
- return_value="AWSAccount",
- ),
patch(
"tasks.jobs.attack_paths.findings.get_node_uid_field",
return_value="arn",
diff --git a/contrib/reverse-proxy/README.md b/contrib/reverse-proxy/README.md
new file mode 100644
index 0000000000..a6387e689a
--- /dev/null
+++ b/contrib/reverse-proxy/README.md
@@ -0,0 +1,64 @@
+# Prowler Reverse Proxy Configuration
+
+Ready-to-use nginx configuration for running Prowler behind a reverse proxy.
+
+## Problem
+
+Prowler's default Docker setup exposes two separate services:
+- **UI** on port 3000
+- **API** on port 8080
+
+This causes CORS issues and authentication failures (especially SAML SSO) when accessed through an external reverse proxy, since the proxy typically exposes a single domain.
+
+## Solution
+
+This adds an nginx container that unifies both services behind a single port, correctly forwarding headers so that Django generates proper URLs for SAML ACS callbacks and API responses.
+
+## Quick Start
+
+From the prowler root directory:
+
+ docker compose -f docker-compose.yml \
+ -f contrib/reverse-proxy/docker-compose.reverse-proxy.yml \
+ up -d
+
+Access Prowler at http://localhost (port 80).
+
+## With an External Reverse Proxy
+
+Point your external reverse proxy to the prowler-nginx container on port 80.
+
+### Environment Variables
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| PROWLER_PROXY_PORT | 80 | Port exposed by the nginx proxy |
+
+### Example: Traefik
+
+ services:
+ nginx:
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.prowler.rule=Host(`prowler.example.com`)"
+ - "traefik.http.routers.prowler.tls.certresolver=letsencrypt"
+ - "traefik.http.services.prowler.loadbalancer.server.port=80"
+
+### Example: Caddy
+
+ prowler.example.com {
+ reverse_proxy prowler-nginx:80
+ }
+
+## SAML SSO
+
+If using SAML SSO behind a reverse proxy, also set the SAML_ACS_BASE_URL environment variable:
+
+ SAML_ACS_BASE_URL=https://prowler.example.com
+
+## Architecture
+
+ Internet -> External Reverse Proxy -> prowler-nginx:80
+ |-- /api/* -> prowler-api:8080
+ |-- /accounts/saml/ -> prowler-api:8080
+ +-- /* -> prowler-ui:3000
diff --git a/contrib/reverse-proxy/docker-compose.reverse-proxy.yml b/contrib/reverse-proxy/docker-compose.reverse-proxy.yml
new file mode 100644
index 0000000000..08c52f3558
--- /dev/null
+++ b/contrib/reverse-proxy/docker-compose.reverse-proxy.yml
@@ -0,0 +1,42 @@
+# Prowler Reverse Proxy - Docker Compose Override
+#
+# Use this alongside the main docker-compose.yml to add an nginx
+# reverse proxy that unifies UI and API behind a single port.
+#
+# Usage:
+# docker compose -f docker-compose.yml -f contrib/reverse-proxy/docker-compose.reverse-proxy.yml up -d
+#
+# Then access Prowler at http://localhost (port 80) or configure
+# your external reverse proxy (Traefik, Caddy, Cloudflare Tunnel,
+# Pangolin, etc.) to point to this container on port 80.
+#
+# For HTTPS with your own certs, see the README in this directory.
+#
+# Fixes: https://github.com/prowler-cloud/prowler/issues/8516
+
+services:
+ nginx:
+ image: nginx:alpine
+ container_name: prowler-nginx
+ restart: unless-stopped
+ ports:
+ - "${PROWLER_PROXY_PORT:-80}:80"
+ volumes:
+ - ./contrib/reverse-proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
+ depends_on:
+ - prowler-ui
+ - prowler-api
+ networks:
+ - prowler-network
+
+ # Override UI to not expose port externally (nginx handles it)
+ prowler-ui:
+ ports: !reset []
+
+ # Override API to not expose port externally (nginx handles it)
+ prowler-api:
+ ports: !reset []
+
+networks:
+ prowler-network:
+ driver: bridge
diff --git a/contrib/reverse-proxy/nginx.conf b/contrib/reverse-proxy/nginx.conf
new file mode 100644
index 0000000000..58520295bc
--- /dev/null
+++ b/contrib/reverse-proxy/nginx.conf
@@ -0,0 +1,70 @@
+# Prowler Reverse Proxy Configuration
+# Routes both UI and API through a single endpoint
+#
+# Usage: See docker-compose.reverse-proxy.yml
+# Fixes: https://github.com/prowler-cloud/prowler/issues/8516
+
+upstream prowler-ui {
+ server prowler-ui:3000;
+}
+
+upstream prowler-api {
+ server prowler-api:8080;
+}
+
+server {
+ listen 80;
+ server_name _;
+
+ # Security headers
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+ # API requests — proxy to prowler-api
+ location /api/ {
+ proxy_pass http://prowler-api/api/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_read_timeout 300s;
+ proxy_connect_timeout 10s;
+
+ # Handle large scan payloads
+ client_max_body_size 50m;
+ }
+
+ # SAML endpoints — proxy to prowler-api
+ location /accounts/saml/ {
+ proxy_pass http://prowler-api/accounts/saml/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ }
+
+ # Everything else — proxy to prowler-ui
+ location / {
+ proxy_pass http://prowler-ui/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+
+ # WebSocket support for Next.js HMR (dev) and live updates
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # Health check endpoint
+ location /health {
+ access_log off;
+ return 200 "ok\n";
+ add_header Content-Type text/plain;
+ }
+}
diff --git a/docs/developer-guide/lighthouse-architecture.mdx b/docs/developer-guide/lighthouse-architecture.mdx
index 38ee50c5b8..e8acc6278c 100644
--- a/docs/developer-guide/lighthouse-architecture.mdx
+++ b/docs/developer-guide/lighthouse-architecture.mdx
@@ -15,8 +15,7 @@ This document describes the internal architecture of Prowler Lighthouse AI, enab
Lighthouse AI operates as a Langchain-based agent that connects Large Language Models (LLMs) with Prowler security data through the Model Context Protocol (MCP).
-
-
+
### Three-Tier Architecture
diff --git a/docs/docs.json b/docs/docs.json
index c76b7174ef..82c23ba42f 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -12,6 +12,24 @@
"dark": "/images/prowler-logo-white.png",
"light": "/images/prowler-logo-black.png"
},
+ "contextual": {
+ "options": [
+ "copy",
+ "view",
+ {
+ "title": "Request a feature",
+ "description": "Open a feature request on GitHub",
+ "icon": "plus",
+ "href": "https://github.com/prowler-cloud/prowler/issues/new?template=feature-request.yml"
+ },
+ {
+ "title": "Report an issue",
+ "description": "Open a bug report on GitHub",
+ "icon": "bug",
+ "href": "https://github.com/prowler-cloud/prowler/issues/new?template=bug_report.yml"
+ }
+ ]
+ },
"navigation": {
"tabs": [
{
@@ -133,6 +151,7 @@
]
},
"user-guide/tutorials/prowler-app-attack-paths",
+ "user-guide/tutorials/prowler-app-finding-groups",
"user-guide/tutorials/prowler-cloud-public-ips",
{
"group": "Tutorials",
diff --git a/docs/getting-started/installation/prowler-app.mdx b/docs/getting-started/installation/prowler-app.mdx
index ea4029c234..c27eb2d35a 100644
--- a/docs/getting-started/installation/prowler-app.mdx
+++ b/docs/getting-started/installation/prowler-app.mdx
@@ -121,8 +121,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
-PROWLER_UI_VERSION="5.23.0"
-PROWLER_API_VERSION="5.23.0"
+PROWLER_UI_VERSION="5.24.0"
+PROWLER_API_VERSION="5.24.0"
```
diff --git a/docs/getting-started/products/prowler-lighthouse-ai.mdx b/docs/getting-started/products/prowler-lighthouse-ai.mdx
index 6f197ae546..4e2eed8538 100644
--- a/docs/getting-started/products/prowler-lighthouse-ai.mdx
+++ b/docs/getting-started/products/prowler-lighthouse-ai.mdx
@@ -59,6 +59,10 @@ Prowler Lighthouse AI is powerful, but there are limitations:
- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse AI will error out. Refresh and log back in to continue.
- **Response quality**: The response quality depends on the selected LLM provider and model. Choose models with strong tool-calling capabilities for best results. We recommend `gpt-5` model from OpenAI.
+## Architecture
+
+
+
## Extending Lighthouse AI
Lighthouse AI retrieves data through Prowler MCP. To add new capabilities, extend the Prowler MCP Server with additional tools and Lighthouse AI discovers them automatically.
diff --git a/docs/getting-started/products/prowler-mcp.mdx b/docs/getting-started/products/prowler-mcp.mdx
index ea6fe8fcc0..762b088326 100644
--- a/docs/getting-started/products/prowler-mcp.mdx
+++ b/docs/getting-started/products/prowler-mcp.mdx
@@ -46,8 +46,7 @@ Search and retrieve official Prowler documentation:
The following diagram illustrates the Prowler MCP Server architecture and its integration points:
-
-
+
The architecture shows how AI assistants connect through the MCP protocol to access Prowler's three main components:
- Prowler Cloud/App for security operations
diff --git a/docs/images/finding-groups-drawer.png b/docs/images/finding-groups-drawer.png
new file mode 100644
index 0000000000..2c07f63249
Binary files /dev/null and b/docs/images/finding-groups-drawer.png differ
diff --git a/docs/images/finding-groups-expanded.png b/docs/images/finding-groups-expanded.png
new file mode 100644
index 0000000000..677df0020f
Binary files /dev/null and b/docs/images/finding-groups-expanded.png differ
diff --git a/docs/images/finding-groups-list.png b/docs/images/finding-groups-list.png
new file mode 100644
index 0000000000..e70d2bb969
Binary files /dev/null and b/docs/images/finding-groups-list.png differ
diff --git a/docs/images/finding-groups-other-findings.png b/docs/images/finding-groups-other-findings.png
new file mode 100644
index 0000000000..35605a7710
Binary files /dev/null and b/docs/images/finding-groups-other-findings.png differ
diff --git a/docs/images/lighthouse-architecture-dark.png b/docs/images/lighthouse-architecture-dark.png
deleted file mode 100644
index 0ed77712c5..0000000000
Binary files a/docs/images/lighthouse-architecture-dark.png and /dev/null differ
diff --git a/docs/images/lighthouse-architecture-light.png b/docs/images/lighthouse-architecture-light.png
deleted file mode 100644
index 076240ccb2..0000000000
Binary files a/docs/images/lighthouse-architecture-light.png and /dev/null differ
diff --git a/docs/images/lighthouse-architecture.mmd b/docs/images/lighthouse-architecture.mmd
new file mode 100644
index 0000000000..47407544e9
--- /dev/null
+++ b/docs/images/lighthouse-architecture.mmd
@@ -0,0 +1,37 @@
+flowchart TB
+ browser([Browser])
+
+ subgraph NEXTJS["Next.js Server"]
+ route["API Route (auth + context assembly)"]
+ agent["LangChain Agent"]
+
+ subgraph TOOLS["Agent Tools"]
+ metatools["Meta-tools describe_tool / execute_tool / load_skill"]
+ end
+
+ mcpclient["MCP Client (HTTP transport)"]
+ end
+
+ llm["LLM Provider (OpenAI / Bedrock / OpenAI-compatible)"]
+
+ subgraph MCP["Prowler MCP Server"]
+ app_tools["prowler_app_* tools (auth required)"]
+ hub_tools["prowler_hub_* tools (no auth)"]
+ docs_tools["prowler_docs_* tools (no auth)"]
+ end
+
+ api["Prowler API"]
+ hub["hub.prowler.com"]
+ docs["docs.prowler.com (Mintlify)"]
+
+ browser <-->|SSE stream| route
+ route --> agent
+ agent <-->|LLM API| llm
+ agent --> metatools
+ metatools --> mcpclient
+ mcpclient -->|MCP HTTP · Bearer token for prowler_app_* only| app_tools
+ mcpclient -->|MCP HTTP| hub_tools
+ mcpclient -->|MCP HTTP| docs_tools
+ app_tools -->|REST| api
+ hub_tools -->|REST| hub
+ docs_tools -->|REST| docs
diff --git a/docs/images/lighthouse-architecture.png b/docs/images/lighthouse-architecture.png
new file mode 100644
index 0000000000..b2e8266154
Binary files /dev/null and b/docs/images/lighthouse-architecture.png differ
diff --git a/docs/images/products/prowler-app-architecture.mmd b/docs/images/products/prowler-app-architecture.mmd
index bfc687b82e..0c13d580c3 100644
--- a/docs/images/products/prowler-app-architecture.mmd
+++ b/docs/images/products/prowler-app-architecture.mmd
@@ -23,6 +23,8 @@ flowchart TB
user --> ui
user --> cli
ui -->|REST| api
+ ui -->|MCP HTTP| mcp
+ mcp -->|REST| api
api --> pg
api --> valkey
beat -->|enqueue jobs| valkey
@@ -31,7 +33,5 @@ flowchart TB
worker -->|Attack Paths| neo4j
worker -->|invokes| sdk
cli --> sdk
- api -. AI tools .-> mcp
- mcp -. context .-> api
sdk --> providers
diff --git a/docs/images/products/prowler-app-architecture.png b/docs/images/products/prowler-app-architecture.png
index 19bbfab0ee..6b0dacb776 100644
Binary files a/docs/images/products/prowler-app-architecture.png and b/docs/images/products/prowler-app-architecture.png differ
diff --git a/docs/images/prowler_mcp_schema.mmd b/docs/images/prowler_mcp_schema.mmd
new file mode 100644
index 0000000000..96973546f6
--- /dev/null
+++ b/docs/images/prowler_mcp_schema.mmd
@@ -0,0 +1,29 @@
+flowchart LR
+ subgraph HOSTS["MCP Hosts"]
+ chat["Chat Interfaces (Claude Desktop, LobeChat)"]
+ ide["IDEs and Code Editors (Claude Code, Cursor)"]
+ apps["Other AI Applications (5ire, custom agents)"]
+ end
+
+ subgraph MCP["Prowler MCP Server"]
+ app_tools["prowler_app_* tools (JWT or API key auth) Findings · Providers · Scans Resources · Muting · Compliance Attack Paths"]
+ hub_tools["prowler_hub_* tools (no auth) Checks Catalog · Check Code Fixers · Compliance Frameworks"]
+ docs_tools["prowler_docs_* tools (no auth) Search · Document Retrieval"]
+ end
+
+ api["Prowler API (REST)"]
+ hub["hub.prowler.com (REST)"]
+ docs["docs.prowler.com (Mintlify)"]
+
+ chat -->|STDIO or HTTP| app_tools
+ chat -->|STDIO or HTTP| hub_tools
+ chat -->|STDIO or HTTP| docs_tools
+ ide -->|STDIO or HTTP| app_tools
+ ide -->|STDIO or HTTP| hub_tools
+ ide -->|STDIO or HTTP| docs_tools
+ apps -->|STDIO or HTTP| app_tools
+ apps -->|STDIO or HTTP| hub_tools
+ apps -->|STDIO or HTTP| docs_tools
+ app_tools -->|REST| api
+ hub_tools -->|REST| hub
+ docs_tools -->|REST| docs
diff --git a/docs/images/prowler_mcp_schema.png b/docs/images/prowler_mcp_schema.png
new file mode 100644
index 0000000000..8a8884fa5e
Binary files /dev/null and b/docs/images/prowler_mcp_schema.png differ
diff --git a/docs/images/prowler_mcp_schema_dark.png b/docs/images/prowler_mcp_schema_dark.png
deleted file mode 100644
index 7771557601..0000000000
Binary files a/docs/images/prowler_mcp_schema_dark.png and /dev/null differ
diff --git a/docs/images/prowler_mcp_schema_light.png b/docs/images/prowler_mcp_schema_light.png
deleted file mode 100644
index c542d84ed2..0000000000
Binary files a/docs/images/prowler_mcp_schema_light.png and /dev/null differ
diff --git a/docs/user-guide/providers/aws/regions-and-partitions.mdx b/docs/user-guide/providers/aws/regions-and-partitions.mdx
index 02ae8fe6ab..377612013e 100644
--- a/docs/user-guide/providers/aws/regions-and-partitions.mdx
+++ b/docs/user-guide/providers/aws/regions-and-partitions.mdx
@@ -33,6 +33,41 @@ To scan a particular AWS region with Prowler, use:
prowler aws -f/--region eu-west-1 us-east-1
```
+### Excluding Specific Regions
+
+To scan all supported AWS regions except a specific subset, use the `--excluded-region` flag:
+
+```console
+prowler aws --excluded-region eu-west-1 me-south-1
+```
+
+You can also configure the exclusion list with the `PROWLER_AWS_DISALLOWED_REGIONS` environment variable as a comma-separated list:
+
+```console
+export PROWLER_AWS_DISALLOWED_REGIONS="eu-west-1,me-south-1"
+prowler aws
+```
+
+Or with the AWS provider configuration in `config.yaml`:
+
+```yaml
+aws:
+ disallowed_regions:
+ - eu-west-1
+ - me-south-1
+```
+
+When more than one source is set, precedence is:
+
+1. `--excluded-region`
+2. `PROWLER_AWS_DISALLOWED_REGIONS`
+3. `aws.disallowed_regions` in `config.yaml`
+
+
+For self-hosted App or API-triggered scans, set `PROWLER_AWS_DISALLOWED_REGIONS` in the runtime environment of the backend scan containers such as `api` and `worker`. The `ui` container does not enforce AWS region selection.
+
+
+
### AWS Credentials Configuration
For details on configuring AWS credentials, refer to the following [Botocore](https://github.com/boto/botocore) [file](https://github.com/boto/botocore/blob/22a19ea7c4c2c4dd7df4ab8c32733cba0c7597a4/botocore/data/partitions.json).
diff --git a/docs/user-guide/tutorials/prowler-app-finding-groups.mdx b/docs/user-guide/tutorials/prowler-app-finding-groups.mdx
new file mode 100644
index 0000000000..e1bf83a50f
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-app-finding-groups.mdx
@@ -0,0 +1,119 @@
+---
+title: 'Finding Groups'
+description: 'Organize and triage security findings by check to reduce noise and prioritize remediation effectively.'
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Finding Groups transforms security findings triage by grouping them by check instead of displaying a flat list. This dramatically reduces noise and enables faster, more effective prioritization.
+
+## Triage Challenges with Flat Finding Lists
+
+A real cloud environment produces thousands of findings per scan. A flat list makes it impossible to triage effectively:
+
+- **Signal buried in noise**: the same misconfiguration repeated across 200 resources shows up as 200 rows, burying the signal in repetitive data
+- **Prioritization guesswork**: without grouping, understanding which issues affect the most resources requires manual counting and correlation
+- **Tedious muting**: muting a false positive globally requires manually acting on each individual finding across the list
+- **Lost context**: when investigating a single resource, related findings are scattered across the same flat list, making it hard to see the full picture
+
+## How Finding Groups Addresses These Challenges
+
+Finding Groups addresses these challenges by intelligently grouping findings by check.
+
+### Grouped View at a Glance
+
+Each row represents a single check title with key information immediately visible:
+
+- **Severity** indicator for quick risk assessment
+- **Impacted providers** showing which cloud platforms are affected
+- **X of Y impacted resources** counter displaying how many resources fail this check
+
+For example, `Vercel project has the Web Application Firewall enabled` across every affected project collapses to a single row — not one per project. Sort or filter by severity, provider, or status at the group level to triage top-down instead of drowning in per-resource rows.
+
+
+
+### Expanding Groups for Details
+
+Expand any group inline to see the failing resources with detailed information:
+
+| Column | Description |
+|--------|-------------|
+| **UID** | Unique identifier for the resource |
+| **Service** | The cloud service the resource belongs to |
+| **Region** | Geographic region where the resource is deployed |
+| **Severity** | Risk level of the finding |
+| **Provider** | Cloud provider (AWS, Azure, GCP, Kubernetes, etc.) |
+| **Last Seen** | When the finding was last detected |
+| **Failing For** | Duration the resource has been in a failing state |
+
+
+
+### Resource Detail Drawer
+
+Select any resource to open the detail drawer with full finding context:
+
+- **Risk**: the security risk associated with this finding
+- **Description**: detailed explanation of what was detected
+- **Status Extended**: additional status information and context
+- **Remediation**: step-by-step guidance to resolve the issue
+- **View in Prowler Hub**: direct link to explore the check in Prowler Hub
+- **Analyze This Finding With Lighthouse AI**: one-click AI-powered analysis for deeper insights
+
+
+
+### Bulk Actions
+
+Bulk-mute an entire group instead of chasing duplicates across the list. This is especially useful for:
+
+- Known false positives that appear across many resources
+- Findings in development or test environments
+- Accepted risks that have been documented and approved
+
+
+Muting findings does not resolve underlying security issues. Review each finding carefully before muting to ensure it represents an acceptable risk or has been properly addressed.
+
+
+## Other Findings for This Resource
+
+Inside the resource detail drawer, the **Other Findings For This Resource** tab lists every finding that hits the same resource — passing, failing, and muted — alongside the one currently being reviewed.
+
+
+
+### Why This Matters
+
+When reviewing "WAF not enabled" on a Vercel project, the tab immediately shows:
+
+- Skew protection status
+- Rate limiting configuration
+- IP blocking settings
+- Custom firewall rules
+- Password protection findings
+
+All for that same project, without navigating back to the main list and filtering by resource UID.
+
+### Complete Context Within the Drawer
+
+Pair the Other Findings tab with:
+
+- **Scans tab**: scan history for this resource
+- **Events tab**: changes and events over time
+
+This provides full context without leaving the drawer.
+
+## Best Practices
+
+1. **Start with high severity groups**: focus on critical and high severity groups first for maximum impact.
+2. **Use filters strategically**: filter by provider or status at the group level to narrow the triage scope.
+3. **Leverage bulk mute**: when a finding represents a confirmed false positive, mute the entire group at once.
+4. **Check related findings**: review the Other Findings tab to understand the full security posture of a resource.
+5. **Track failure duration**: use the "Failing For" column to prioritize long-standing issues that may indicate systemic problems.
+
+## Getting Started
+
+1. Navigate to the **Findings** section in Prowler Cloud/App.
+2. Toggle to the **Grouped View** to see findings organized by check.
+3. Select any group row to expand and see affected resources.
+4. Select a resource to open the detail drawer with full context.
+5. Use the **Other Findings For This Resource** tab to see all findings for that resource.
diff --git a/docs/user-guide/tutorials/prowler-app-lighthouse.mdx b/docs/user-guide/tutorials/prowler-app-lighthouse.mdx
index cf4886d2ee..b119893cb5 100644
--- a/docs/user-guide/tutorials/prowler-app-lighthouse.mdx
+++ b/docs/user-guide/tutorials/prowler-app-lighthouse.mdx
@@ -25,8 +25,7 @@ Behind the scenes, Lighthouse AI works as follows:
Lighthouse AI supports multiple LLM providers including OpenAI, Amazon Bedrock, and OpenAI-compatible services. For configuration details, see [Using Multiple LLM Providers with Lighthouse](/user-guide/tutorials/prowler-app-lighthouse-multi-llm).
-
-
+
diff --git a/poetry.lock b/poetry.lock
index 7d5ccdf355..3b2ba9c4a5 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -1267,19 +1267,19 @@ typing-extensions = ">=4.6.0"
[[package]]
name = "azure-mgmt-resource"
-version = "23.3.0"
+version = "24.0.0"
description = "Microsoft Azure Resource Management Client Library for Python"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "azure_mgmt_resource-23.3.0-py3-none-any.whl", hash = "sha256:ab216ee28e29db6654b989746e0c85a1181f66653929d2cb6e48fba66d9af323"},
- {file = "azure_mgmt_resource-23.3.0.tar.gz", hash = "sha256:fc4f1fd8b6aad23f8af4ed1f913df5f5c92df117449dc354fea6802a2829fea4"},
+ {file = "azure_mgmt_resource-24.0.0-py3-none-any.whl", hash = "sha256:27b32cd223e2784269f5a0db3c282042886ee4072d79cedc638438ece7cd0df4"},
+ {file = "azure_mgmt_resource-24.0.0.tar.gz", hash = "sha256:cf6b8995fcdd407ac9ff1dd474087129429a1d90dbb1ac77f97c19b96237b265"},
]
[package.dependencies]
azure-common = ">=1.1"
-azure-mgmt-core = ">=1.3.2"
+azure-mgmt-core = ">=1.5.0"
isodate = ">=0.6.1"
typing-extensions = ">=4.6.0"
@@ -1425,6 +1425,64 @@ typing-extensions = ">=4.6.0"
[package.extras]
aio = ["azure-core[aio] (>=1.30.0)"]
+[[package]]
+name = "backports-datetime-fromisoformat"
+version = "2.0.3"
+description = "Backport of Python 3.11's datetime.fromisoformat"
+optional = false
+python-versions = ">3"
+groups = ["dev"]
+markers = "python_version == \"3.10\""
+files = [
+ {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e81b26497a17c29595bc7df20bc6a872ceea5f8c9d6537283945d4b6396aec10"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:5ba00ead8d9d82fd6123eb4891c566d30a293454e54e32ff7ead7644f5f7e575"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:24d574cb4072e1640b00864e94c4c89858033936ece3fc0e1c6f7179f120d0a8"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9735695a66aad654500b0193525e590c693ab3368478ce07b34b443a1ea5e824"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63d39709e17eb72685d052ac82acf0763e047f57c86af1b791505b1fec96915d"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1ea2cc84224937d6b9b4c07f5cb7c667f2bde28c255645ba27f8a675a7af8234"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4024e6d35a9fdc1b3fd6ac7a673bd16cb176c7e0b952af6428b7129a70f72cce"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5e2dcc94dc9c9ab8704409d86fcb5236316e9dcef6feed8162287634e3568f4c"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fa2de871801d824c255fac7e5e7e50f2be6c9c376fd9268b40c54b5e9da91f42"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:1314d4923c1509aa9696712a7bc0c7160d3b7acf72adafbbe6c558d523f5d491"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b750ecba3a8815ad8bc48311552f3f8ab99dd2326d29df7ff670d9c49321f48f"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d5117dce805d8a2f78baeddc8c6127281fa0a5e2c40c6dd992ba6b2b367876"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb35f607bd1cbe37b896379d5f5ed4dc298b536f4b959cb63180e05cacc0539d"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:61c74710900602637d2d145dda9720c94e303380803bf68811b2a151deec75c2"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ece59af54ebf67ecbfbbf3ca9066f5687879e36527ad69d8b6e3ac565d565a62"},
+ {file = "backports_datetime_fromisoformat-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:d0a7c5f875068efe106f62233bc712d50db4d07c13c7db570175c7857a7b5dbd"},
+ {file = "backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039"},
+ {file = "backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06"},
+ {file = "backports_datetime_fromisoformat-2.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7100adcda5e818b5a894ad0626e38118bb896a347f40ebed8981155675b9ba7b"},
+ {file = "backports_datetime_fromisoformat-2.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e410383f5d6a449a529d074e88af8bc80020bb42b402265f9c02c8358c11da5"},
+ {file = "backports_datetime_fromisoformat-2.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2797593760da6bcc32c4a13fa825af183cd4bfd333c60b3dbf84711afca26ef"},
+ {file = "backports_datetime_fromisoformat-2.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35a144fd681a0bea1013ccc4cd3fd4dc758ea17ee23dca019c02b82ec46fc0c4"},
+ {file = "backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d"},
+]
+
[[package]]
name = "bandit"
version = "1.8.3"
@@ -3350,23 +3408,19 @@ files = [
[[package]]
name = "marshmallow"
-version = "3.26.2"
+version = "4.3.0"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
groups = ["dev"]
files = [
- {file = "marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73"},
- {file = "marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57"},
+ {file = "marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46"},
+ {file = "marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880"},
]
[package.dependencies]
-packaging = ">=17.0"
-
-[package.extras]
-dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"]
-docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"]
-tests = ["pytest", "simplejson"]
+backports-datetime-fromisoformat = {version = "*", markers = "python_version < \"3.11\""}
+typing-extensions = {version = "*", markers = "python_version < \"3.11\""}
[[package]]
name = "mccabe"
@@ -3662,14 +3716,14 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"]
[[package]]
name = "msgraph-sdk"
-version = "1.23.0"
+version = "1.55.0"
description = "The Microsoft Graph Python SDK"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "msgraph_sdk-1.23.0-py3-none-any.whl", hash = "sha256:58e0047b4ca59fd82022c02cd73fec0170a3d84f3b76721e3db2a0314df9a58a"},
- {file = "msgraph_sdk-1.23.0.tar.gz", hash = "sha256:6dd1ba9a46f5f0ce8599fd9610133adbd9d1493941438b5d3632fce9e55ed607"},
+ {file = "msgraph_sdk-1.55.0-py3-none-any.whl", hash = "sha256:c8e68ebc4b88af5111de312e7fa910a4e76ddf48a4534feadb1fb8a411c48cfc"},
+ {file = "msgraph_sdk-1.55.0.tar.gz", hash = "sha256:6df691a31954a050d26b8a678968017e157d940fb377f2a8a4e17a9741b98756"},
]
[package.dependencies]
@@ -6681,4 +6735,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.13"
-content-hash = "786921163bb46716defae1d9de1df001af2abf17edd3061165638707bcd28ce4"
+content-hash = "09ce4507a464b318702ed8c6a738f3bb1bc4cc6ff5a50a9c2884f560af9ab034"
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index 44c30bb3c5..eff2506c04 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -2,7 +2,23 @@
All notable changes to the **Prowler SDK** are documented in this file.
-## [5.24.0] (Prowler UNRELEASED)
+## [5.24.1] (Prowler v5.24.1)
+
+### 🔄 Changed
+
+- `msgraph-sdk` from 1.23.0 to 1.55.0 and `azure-mgmt-resource` from 23.3.0 to 24.0.0, removing `marshmallow` as is a transitively dev dependency [(#10733)](https://github.com/prowler-cloud/prowler/pull/10733)
+
+### 🐞 Fixed
+
+- Cloudflare account-scoped API tokens failing connection test in the App with `CloudflareUserTokenRequiredError` [(#10723)](https://github.com/prowler-cloud/prowler/pull/10723)
+- `prowler image --registry-list` crashes with `AttributeError` because `ImageProvider.__init__` returns early before registering the global provider [(#10691)](https://github.com/prowler-cloud/prowler/pull/10691)
+- Google Workspace Calendar checks false FAIL on unconfigured settings with secure Google defaults [(#10726)](https://github.com/prowler-cloud/prowler/pull/10726)
+- Google Workspace Drive checks false FAIL on unconfigured settings with secure Google defaults [(#10727)](https://github.com/prowler-cloud/prowler/pull/10727)
+- Cloudflare `validate_credentials` can hang in an infinite pagination loop when the SDK repeats accounts, blocking connection tests [(#10771)](https://github.com/prowler-cloud/prowler/pull/10771)
+
+---
+
+## [5.24.0] (Prowler v5.24.0)
### 🚀 Added
@@ -13,17 +29,22 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `iam_role_access_not_stale_to_bedrock` and `iam_user_access_not_stale_to_bedrock` checks for AWS provider [(#10536)](https://github.com/prowler-cloud/prowler/pull/10536)
- `iam_policy_no_wildcard_marketplace_subscribe` and `iam_inline_policy_no_wildcard_marketplace_subscribe` checks for AWS provider [(#10525)](https://github.com/prowler-cloud/prowler/pull/10525)
- `bedrock_vpc_endpoints_configured` check for AWS provider [(#10591)](https://github.com/prowler-cloud/prowler/pull/10591)
-- `exchange_organization_delicensing_resiliency_enabled` check for m365 provider [(#10608)](https://github.com/prowler-cloud/prowler/pull/10608)
+- `exchange_organization_delicensing_resiliency_enabled` check for M365 provider [(#10608)](https://github.com/prowler-cloud/prowler/pull/10608)
- `entra_conditional_access_policy_mfa_enforced_for_guest_users` check for M365 provider [(#10616)](https://github.com/prowler-cloud/prowler/pull/10616)
-- `entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced` check for m365 provider [(#10618)](https://github.com/prowler-cloud/prowler/pull/10618)
-- `entra_conditional_access_policy_block_unknown_device_platforms` check for m365 provider [(#10615)](https://github.com/prowler-cloud/prowler/pull/10615)
-- Support for external/custom providers, checks, and compliance frameworks without modifying core code [(#10700)](https://github.com/prowler-cloud/prowler/pull/10700)
+- `entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced` check for M365 provider [(#10618)](https://github.com/prowler-cloud/prowler/pull/10618)
+- `entra_conditional_access_policy_block_unknown_device_platforms` check for M365 provider [(#10615)](https://github.com/prowler-cloud/prowler/pull/10615)
+- `--excluded-region` CLI flag, `PROWLER_AWS_DISALLOWED_REGIONS` environment variable, and `aws.disallowed_regions` config entry to skip specific AWS regions during scans [(#10688)](https://github.com/prowler-cloud/prowler/pull/10688)
### 🔄 Changed
- Bump Poetry to `2.3.4` and consolidate SDK workflows onto the `setup-python-poetry` composite action with opt-in lockfile regeneration [(#10681)](https://github.com/prowler-cloud/prowler/pull/10681)
- Normalize Conditional Access platform values in Entra models and simplify platform-based checks [(#10635)](https://github.com/prowler-cloud/prowler/pull/10635)
+### 🐞 Fixed
+
+- `prowler image --registry-list` crashes with `AttributeError` because `ImageProvider.__init__` returns early before registering the global provider [(#10691)](https://github.com/prowler-cloud/prowler/pull/10691)
+- Vercel firewall config handling for team-scoped projects and current API response shapes [(#10695)](https://github.com/prowler-cloud/prowler/pull/10695)
+
---
## [5.23.0] (Prowler v5.23.0)
@@ -788,7 +809,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- S3 `test_connection` uses AWS S3 API `HeadBucket` instead of `GetBucketLocation` [(#8456)](https://github.com/prowler-cloud/prowler/pull/8456)
- Add more validations to Azure Storage models when some values are None to avoid serialization issues [(#8325)](https://github.com/prowler-cloud/prowler/pull/8325)
- `sns_topics_not_publicly_accessible` false positive with `aws:SourceArn` conditions [(#8326)](https://github.com/prowler-cloud/prowler/issues/8326)
-- Remove typo from description req 1.2.3 - Prowler ThreatScore m365 [(#8384)](https://github.com/prowler-cloud/prowler/pull/8384)
+- Remove typo from description req 1.2.3 - Prowler ThreatScore M365 [(#8384)](https://github.com/prowler-cloud/prowler/pull/8384)
- Way of counting FAILED/PASS reqs from `kisa_isms_p_2023_aws` table [(#8382)](https://github.com/prowler-cloud/prowler/pull/8382)
- Use default tenant domain instead of first domain in list for Azure and M365 providers [(#8402)](https://github.com/prowler-cloud/prowler/pull/8402)
- Avoid multiple module error calls in M365 provider [(#8353)](https://github.com/prowler-cloud/prowler/pull/8353)
@@ -829,7 +850,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Title & description wording for `iam_user_accesskey_unused` check for AWS provider [(#8233)](https://github.com/prowler-cloud/prowler/pull/8233)
- Add GitHub provider to lateral panel in documentation and change -h environment variable output [(#8246)](https://github.com/prowler-cloud/prowler/pull/8246)
-- Show `m365_identity_type` and `m365_identity_id` in cloud reports [(#8247)](https://github.com/prowler-cloud/prowler/pull/8247)
+- Show `M365_identity_type` and `M365_identity_id` in cloud reports [(#8247)](https://github.com/prowler-cloud/prowler/pull/8247)
- Ensure `is_service_role` only returns `True` for service roles [(#8274)](https://github.com/prowler-cloud/prowler/pull/8274)
- Update DynamoDB check metadata to fix broken link [(#8273)](https://github.com/prowler-cloud/prowler/pull/8273)
- Show correct count of findings in Dashboard Security Posture page [(#8270)](https://github.com/prowler-cloud/prowler/pull/8270)
@@ -951,9 +972,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Fixed
-- `m365_powershell test_credentials` to use sanitized credentials [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
+- `M365_powershell test_credentials` to use sanitized credentials [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
- `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779)
-- `m365_powershell` to close the PowerShell sessions in msgraph services [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
+- `M365_powershell` to close the PowerShell sessions in msgraph services [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
- `defender_ensure_notify_alerts_severity_is_high`check to accept high or lower severity [(#7862)](https://github.com/prowler-cloud/prowler/pull/7862)
- Replace `Directory.Read.All` permission with `Domain.Read.All` which is more restrictive [(#7888)](https://github.com/prowler-cloud/prowler/pull/7888)
- Split calls to list Azure Functions attributes [(#7778)](https://github.com/prowler-cloud/prowler/pull/7778)
@@ -1027,7 +1048,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- New check `teams_meeting_chat_anonymous_users_disabled` [(#7579)](https://github.com/prowler-cloud/prowler/pull/7579)
- Prowler Threat Score Compliance Framework [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603)
- Documentation for M365 provider [(#7622)](https://github.com/prowler-cloud/prowler/pull/7622)
-- Support for m365 provider in Prowler Dashboard [(#7633)](https://github.com/prowler-cloud/prowler/pull/7633)
+- Support for M365 provider in Prowler Dashboard [(#7633)](https://github.com/prowler-cloud/prowler/pull/7633)
- New check for Modern Authentication enabled for Exchange Online in M365 [(#7636)](https://github.com/prowler-cloud/prowler/pull/7636)
- New check `sharepoint_onedrive_sync_restricted_unmanaged_devices` [(#7589)](https://github.com/prowler-cloud/prowler/pull/7589)
- New check for Additional Storage restricted for Exchange in M365 [(#7638)](https://github.com/prowler-cloud/prowler/pull/7638)
@@ -1037,7 +1058,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- New check for MailTips full enabled for Exchange in M365 [(#7637)](https://github.com/prowler-cloud/prowler/pull/7637)
- New check for Comprehensive Attachments Filter Applied for Defender in M365 [(#7661)](https://github.com/prowler-cloud/prowler/pull/7661)
- Modified check `exchange_mailbox_properties_auditing_enabled` to make it configurable [(#7662)](https://github.com/prowler-cloud/prowler/pull/7662)
-- snapshots to m365 documentation [(#7673)](https://github.com/prowler-cloud/prowler/pull/7673)
+- snapshots to M365 documentation [(#7673)](https://github.com/prowler-cloud/prowler/pull/7673)
- support for static credentials for sending findings to Amazon S3 and AWS Security Hub [(#7322)](https://github.com/prowler-cloud/prowler/pull/7322)
- Prowler ThreatScore for M365 provider [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
- Microsoft User and User Credential auth to reports [(#7681)](https://github.com/prowler-cloud/prowler/pull/7681)
diff --git a/prowler/__main__.py b/prowler/__main__.py
index a0e94a885f..5768be8884 100644
--- a/prowler/__main__.py
+++ b/prowler/__main__.py
@@ -293,6 +293,10 @@ def prowler():
if not args.only_logs:
global_provider.print_credentials()
+ # --registry-list: listing already printed during provider init, exit
+ if getattr(global_provider, "_listing_only", False):
+ sys.exit()
+
# Skip service and check loading for external-tool providers
if provider not in EXTERNAL_TOOL_PROVIDERS:
# Import custom checks from folder
@@ -1338,8 +1342,12 @@ def prowler():
global_provider.identity.audited_regions,
)
if not global_provider.identity.audited_regions
- else global_provider.identity.audited_regions
+ else set(global_provider.identity.audited_regions)
)
+ if global_provider._enabled_regions is not None:
+ security_hub_regions = security_hub_regions.intersection(
+ global_provider._enabled_regions
+ )
security_hub = SecurityHub(
aws_account_id=global_provider.identity.account,
diff --git a/prowler/config/config.py b/prowler/config/config.py
index 1d3bf78303..1530f10e72 100644
--- a/prowler/config/config.py
+++ b/prowler/config/config.py
@@ -39,7 +39,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
-prowler_version = "5.24.0"
+prowler_version = "5.25.0"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml
index c645d6e25b..024cd974f1 100644
--- a/prowler/config/config.yaml
+++ b/prowler/config/config.yaml
@@ -3,6 +3,10 @@ aws:
# AWS Global Configuration
# aws.mute_non_default_regions --> Set to True to muted failed findings in non-default regions for AccessAnalyzer, GuardDuty, SecurityHub, DRS and Config
mute_non_default_regions: False
+ # aws.disallowed_regions --> List of AWS regions to exclude from the scan.
+ # Also settable via the PROWLER_AWS_DISALLOWED_REGIONS environment variable or
+ # the --excluded-region CLI flag. Precedence: CLI > env var > config file.
+ # disallowed_regions: []
# If you want to mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w mutelist.yaml`:
# Mutelist:
# Accounts:
diff --git a/prowler/lib/check/compliance_models.py b/prowler/lib/check/compliance_models.py
index 71e53c5592..0027092a16 100644
--- a/prowler/lib/check/compliance_models.py
+++ b/prowler/lib/check/compliance_models.py
@@ -1,10 +1,11 @@
import importlib.metadata
+import json
import os
import sys
from enum import Enum
from typing import Optional, Union
-from pydantic.v1 import BaseModel, ValidationError, root_validator
+from pydantic.v1 import BaseModel, Field, ValidationError, root_validator
from prowler.lib.check.utils import list_compliance_modules
from prowler.lib.logger import logger
@@ -460,3 +461,482 @@ def load_compliance_framework(
sys.exit(1)
else:
return compliance_framework
+
+
+# ─── Universal Compliance Schema Models (Phase 1-3) ─────────────────────────
+
+
+class OutputFormats(BaseModel):
+ """Flags indicating in which output formats an attribute should be included."""
+
+ csv: bool = True
+ ocsf: bool = True
+
+
+class AttributeMetadata(BaseModel):
+ """Schema descriptor for a single attribute field in a universal compliance framework."""
+
+ key: str
+ label: Optional[str] = None
+ type: str = "str" # str, int, float, list_str, list_dict, bool
+ enum: Optional[list] = None
+ required: bool = False
+ enum_display: Optional[dict] = None # enum_value -> EnumValueDisplay dict
+ enum_order: Optional[list] = None # explicit ordering of enum values
+ chart_label: Optional[str] = None # axis label when used in charts
+ output_formats: OutputFormats = Field(default_factory=OutputFormats)
+
+
+class SplitByConfig(BaseModel):
+ """Column-splitting configuration (e.g. CIS Level 1/Level 2)."""
+
+ field: str
+ values: list
+
+
+class ScoringConfig(BaseModel):
+ """Weighted scoring configuration (e.g. ThreatScore)."""
+
+ risk_field: str
+ weight_field: str
+
+
+class TableLabels(BaseModel):
+ """Custom pass/fail labels for console table rendering."""
+
+ pass_label: str = "PASS"
+ fail_label: str = "FAIL"
+ provider_header: str = "Provider"
+ group_header: Optional[str] = None
+ status_header: str = "Status"
+ title: Optional[str] = None
+ results_title: Optional[str] = None
+ footer_note: Optional[str] = None
+
+
+class TableConfig(BaseModel):
+ """Declarative rendering instructions for the console compliance table."""
+
+ group_by: str
+ split_by: Optional[SplitByConfig] = None
+ scoring: Optional[ScoringConfig] = None
+ labels: Optional[TableLabels] = None
+
+
+class EnumValueDisplay(BaseModel):
+ """Per-enum-value visual metadata for PDF rendering.
+
+ Replaces hardcoded DIMENSION_MAPPING, TIPO_ICONS, nivel colors.
+ """
+
+ label: Optional[str] = None # "Trazabilidad"
+ abbreviation: Optional[str] = None # "T"
+ color: Optional[str] = None # "#4286F4"
+ icon: Optional[str] = None # emoji
+
+
+class ChartConfig(BaseModel):
+ """Declarative chart description for PDF reports."""
+
+ id: str
+ type: str # vertical_bar | horizontal_bar | radar
+ group_by: str # attribute key to group by
+ title: Optional[str] = None
+ x_label: Optional[str] = None
+ y_label: Optional[str] = None
+ value_source: str = "compliance_percent"
+ color_mode: str = "by_value" # by_value | fixed | by_group
+ fixed_color: Optional[str] = None
+
+
+class ScoringFormula(BaseModel):
+ """Weighted scoring formula (e.g. ThreatScore)."""
+
+ risk_field: str # "LevelOfRisk"
+ weight_field: str # "Weight"
+ risk_boost_factor: float = 0.25 # rfac = 1 + factor * risk_level
+
+
+class CriticalRequirementsFilter(BaseModel):
+ """Filter for critical requirements section in PDF reports."""
+
+ filter_field: str # "LevelOfRisk"
+ min_value: Optional[int] = None # 4 (int-based filter)
+ filter_value: Optional[str] = None # "alto" (string-based filter)
+ status_filter: str = "FAIL"
+ title: Optional[str] = None # "Critical Failed Requirements"
+
+
+class ReportFilter(BaseModel):
+ """Default report filtering for PDF generation."""
+
+ only_failed: bool = True
+ include_manual: bool = False
+
+
+class I18nLabels(BaseModel):
+ """Localized labels for PDF report rendering."""
+
+ report_title: Optional[str] = None
+ page_label: str = "Page"
+ powered_by: str = "Powered by Prowler"
+ framework_label: str = "Framework:"
+ version_label: str = "Version:"
+ provider_label: str = "Provider:"
+ description_label: str = "Description:"
+ compliance_score_label: str = "Compliance Score by Sections"
+ requirements_index_label: str = "Requirements Index"
+ detailed_findings_label: str = "Detailed Findings"
+
+
+class PDFConfig(BaseModel):
+ """Declarative PDF report configuration.
+
+ Drives the API report generator from JSON data instead of hardcoded
+ Python config. Colors are hex strings (e.g. '#336699').
+ """
+
+ language: str = "en"
+ logo_filename: Optional[str] = None
+ primary_color: Optional[str] = None
+ secondary_color: Optional[str] = None
+ bg_color: Optional[str] = None
+ sections: Optional[list] = None
+ section_short_names: Optional[dict] = None
+ group_by_field: Optional[str] = None
+ sub_group_by_field: Optional[str] = None
+ section_titles: Optional[dict] = None
+ charts: Optional[list] = None
+ scoring: Optional[ScoringFormula] = None
+ critical_filter: Optional[CriticalRequirementsFilter] = None
+ filter: Optional[ReportFilter] = None
+ labels: Optional[I18nLabels] = None
+
+
+class UniversalComplianceRequirement(BaseModel):
+ """Universal requirement with flat dict-based attributes."""
+
+ id: str
+ description: str
+ name: Optional[str] = None
+ attributes: dict = Field(default_factory=dict)
+ checks: dict[str, list[str]] = Field(default_factory=dict)
+ tactics: Optional[list] = None
+ sub_techniques: Optional[list] = None
+ platforms: Optional[list] = None
+ technique_url: Optional[str] = None
+
+
+class OutputsConfig(BaseModel):
+ """Container for output-related configuration (table, PDF, etc.)."""
+
+ table_config: Optional[TableConfig] = None
+ pdf_config: Optional[PDFConfig] = None
+
+
+class ComplianceFramework(BaseModel):
+ """Universal top-level container for any compliance framework.
+
+ Provider may be explicit (single-provider JSON) or derived from checks
+ keys across all requirements.
+ """
+
+ framework: str
+ name: str
+ provider: Optional[str] = None
+ version: Optional[str] = None
+ description: str
+ icon: Optional[str] = None
+ requirements: list[UniversalComplianceRequirement]
+ attributes_metadata: Optional[list[AttributeMetadata]] = None
+ outputs: Optional[OutputsConfig] = None
+
+ @root_validator
+ # noqa: F841 - since vulture raises unused variable 'cls'
+ def validate_attributes_against_metadata(cls, values): # noqa: F841
+ """Validate every Requirement's attributes dict against attributes_metadata.
+
+ Checks:
+ - Required keys (required=True) must be present in each Requirement.
+ - Enum-constrained keys must have a value within the declared enum list.
+ - Basic type validation (int, float, bool) for non-None values.
+ """
+ metadata = values.get("attributes_metadata")
+ requirements = values.get("requirements", [])
+ if not metadata:
+ return values
+
+ required_keys = {m.key for m in metadata if m.required}
+ valid_keys = {m.key for m in metadata}
+ enum_map = {m.key: m.enum for m in metadata if m.enum}
+ type_map = {m.key: m.type for m in metadata}
+
+ type_checks = {
+ "int": int,
+ "float": (int, float),
+ "bool": bool,
+ }
+
+ errors = []
+ for req in requirements:
+ attrs = req.attributes
+
+ # Required keys
+ for key in required_keys:
+ if key not in attrs or attrs[key] is None:
+ errors.append(
+ f"Requirement '{req.id}': missing required attribute '{key}'"
+ )
+
+ # Unknown keys — anything outside the declared schema is a typo or drift
+ unknown_keys = set(attrs) - valid_keys
+ for key in sorted(unknown_keys):
+ errors.append(
+ f"Requirement '{req.id}': unknown attribute '{key}' "
+ f"(not declared in attributes_metadata)"
+ )
+
+ # Enum validation
+ for key, allowed in enum_map.items():
+ if key in attrs and attrs[key] is not None:
+ if attrs[key] not in allowed:
+ errors.append(
+ f"Requirement '{req.id}': attribute '{key}' value "
+ f"'{attrs[key]}' not in {allowed}"
+ )
+
+ # Type validation for non-string types
+ for key in attrs:
+ if key not in valid_keys or attrs[key] is None:
+ continue
+ expected_type = type_map.get(key, "str")
+ py_type = type_checks.get(expected_type)
+ if py_type and not isinstance(attrs[key], py_type):
+ errors.append(
+ f"Requirement '{req.id}': attribute '{key}' expected "
+ f"type {expected_type}, got {type(attrs[key]).__name__}"
+ )
+
+ if errors:
+ detail = "\n ".join(errors)
+ raise ValueError(f"attributes_metadata validation failed:\n {detail}")
+
+ return values
+
+ def get_providers(self) -> list:
+ """Derive the set of providers this framework supports.
+
+ Inspects checks keys across all requirements. Falls back to the
+ explicit provider field for single-provider frameworks with no
+ requirement-level checks.
+ """
+ providers = set()
+ for req in self.requirements:
+ providers.update(k.lower() for k in req.checks.keys())
+ if self.provider and not providers:
+ providers.add(self.provider.lower())
+ return sorted(providers)
+
+ def supports_provider(self, provider: str) -> bool:
+ """Return True if this framework has checks for the given provider."""
+ provider_lower = provider.lower()
+ for req in self.requirements:
+ if any(k.lower() == provider_lower for k in req.checks.keys()):
+ return True
+ return self.provider is not None and self.provider.lower() == provider_lower
+
+
+# ─── Legacy-to-Universal Adapter (Phase 2) ──────────────────────────────────
+
+
+def _infer_attribute_metadata(legacy: Compliance) -> Optional[list[AttributeMetadata]]:
+ """Introspect the first requirement's attribute model to build attributes_metadata."""
+ try:
+ if not legacy.Requirements:
+ return None
+
+ first_req = legacy.Requirements[0]
+
+ # MITRE requirements have Tactics at top level, not in Attributes
+ if isinstance(first_req, Mitre_Requirement):
+ return None
+
+ if not first_req.Attributes:
+ return None
+
+ sample_attr = first_req.Attributes[0]
+ metadata = []
+
+ for field_name, field_obj in sample_attr.__fields__.items():
+ field_type = field_obj.outer_type_
+ type_str = "str"
+ enum_values = None
+
+ origin = getattr(field_type, "__origin__", None)
+ if field_type is int:
+ type_str = "int"
+ elif field_type is float:
+ type_str = "float"
+ elif field_type is bool:
+ type_str = "bool"
+ elif origin is list:
+ args = getattr(field_type, "__args__", ())
+ if args and args[0] is dict:
+ type_str = "list_dict"
+ else:
+ type_str = "list_str"
+ elif isinstance(field_type, type) and issubclass(field_type, Enum):
+ type_str = "str"
+ enum_values = [e.value for e in field_type]
+
+ metadata.append(
+ AttributeMetadata(
+ key=field_name,
+ type=type_str,
+ enum=enum_values,
+ required=field_obj.required,
+ )
+ )
+
+ return metadata
+ except Exception:
+ return None
+
+
+def adapt_legacy_to_universal(legacy: Compliance) -> ComplianceFramework:
+ """Convert a legacy Compliance object to a ComplianceFramework."""
+ universal_requirements = []
+ legacy_provider_key = legacy.Provider.lower()
+
+ for req in legacy.Requirements:
+ req_checks = {legacy_provider_key: list(req.Checks)} if req.Checks else {}
+ if isinstance(req, Mitre_Requirement):
+ # For MITRE, promote special fields and store raw attributes
+ raw_attrs = [attr.dict() for attr in req.Attributes]
+ attrs = {"_raw_attributes": raw_attrs}
+ universal_requirements.append(
+ UniversalComplianceRequirement(
+ id=req.Id,
+ description=req.Description,
+ name=req.Name,
+ attributes=attrs,
+ checks=req_checks,
+ tactics=req.Tactics,
+ sub_techniques=req.SubTechniques,
+ platforms=req.Platforms,
+ technique_url=req.TechniqueURL,
+ )
+ )
+ else:
+ # Standard requirement: flatten first attribute to dict
+ if req.Attributes:
+ attrs = req.Attributes[0].dict()
+ else:
+ attrs = {}
+ universal_requirements.append(
+ UniversalComplianceRequirement(
+ id=req.Id,
+ description=req.Description,
+ name=req.Name,
+ attributes=attrs,
+ checks=req_checks,
+ )
+ )
+
+ inferred_metadata = _infer_attribute_metadata(legacy)
+
+ return ComplianceFramework(
+ framework=legacy.Framework,
+ name=legacy.Name,
+ provider=legacy.Provider,
+ version=legacy.Version,
+ description=legacy.Description,
+ requirements=universal_requirements,
+ attributes_metadata=inferred_metadata,
+ )
+
+
+def load_compliance_framework_universal(path: str) -> ComplianceFramework:
+ """Load a compliance JSON as a ComplianceFramework, handling both new and legacy formats."""
+ try:
+ with open(path, "r") as f:
+ data = json.load(f)
+
+ if "attributes_metadata" in data or "requirements" in data:
+ # New universal format — parse directly
+ return ComplianceFramework(**data)
+ else:
+ # Legacy format — parse as Compliance, then adapt
+ legacy = Compliance(**data)
+ return adapt_legacy_to_universal(legacy)
+ except Exception as e:
+ logger.error(
+ f"Failed to load universal compliance framework from {path}: "
+ f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}] -- {e}"
+ )
+ return None
+
+
+def _load_jsons_from_dir(dir_path: str, provider: str, bulk: dict) -> None:
+ """Scan *dir_path* for JSON files and add matching frameworks to *bulk*."""
+ for filename in os.listdir(dir_path):
+ file_path = os.path.join(dir_path, filename)
+ if not (
+ os.path.isfile(file_path)
+ and filename.endswith(".json")
+ and os.stat(file_path).st_size > 0
+ ):
+ continue
+ framework_name = filename.split(".json")[0]
+ if framework_name in bulk:
+ continue
+ fw = load_compliance_framework_universal(file_path)
+ if fw is None:
+ continue
+ if fw.provider and fw.provider.lower() == provider.lower():
+ bulk[framework_name] = fw
+ elif fw.supports_provider(provider):
+ bulk[framework_name] = fw
+
+
+def get_bulk_compliance_frameworks_universal(provider: str) -> dict:
+ """Bulk load all compliance frameworks relevant to the given provider.
+
+ Scans:
+
+ 1. The **top-level** ``prowler/compliance/`` directory for multi-provider
+ JSONs (``Checks`` keyed by provider, no ``Provider`` field).
+ 2. Every **provider sub-directory** (``prowler/compliance/{p}/``) so that
+ single-provider JSONs are also picked up.
+
+ A framework is included when its explicit ``Provider`` matches
+ (case-insensitive) **or** any requirement has dict-style ``Checks``
+ with a key for *provider*.
+ """
+ bulk = {}
+ try:
+ available_modules = list_compliance_modules()
+
+ # Resolve the compliance root once (parent of provider sub-dirs).
+ compliance_root = None
+ seen_paths = set()
+
+ for module in available_modules:
+ dir_path = f"{module.module_finder.path}/{module.name.split('.')[-1]}"
+ if not os.path.isdir(dir_path) or dir_path in seen_paths:
+ continue
+ seen_paths.add(dir_path)
+
+ # Remember the root the first time we see a valid sub-dir.
+ if compliance_root is None:
+ compliance_root = module.module_finder.path
+
+ _load_jsons_from_dir(dir_path, provider, bulk)
+
+ # Also scan top-level compliance/ for provider-agnostic JSONs.
+ if compliance_root and os.path.isdir(compliance_root):
+ _load_jsons_from_dir(compliance_root, provider, bulk)
+
+ except Exception as e:
+ logger.error(f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}] -- {e}")
+ return bulk
diff --git a/prowler/providers/aws/aws_provider.py b/prowler/providers/aws/aws_provider.py
index 9a248e826f..cf9356f2fb 100644
--- a/prowler/providers/aws/aws_provider.py
+++ b/prowler/providers/aws/aws_provider.py
@@ -111,6 +111,7 @@ class AwsProvider(Provider):
mfa: bool = False,
profile: str = None,
regions: set = set(),
+ excluded_regions: set = None,
organizations_role_arn: str = None,
scan_unused_services: bool = False,
resource_tags: list[str] = [],
@@ -136,6 +137,10 @@ class AwsProvider(Provider):
- mfa: A boolean indicating whether MFA is enabled.
- profile: The name of the AWS CLI profile to use.
- regions: A set of regions to audit.
+ - excluded_regions: A set of regions to skip during the scan. Applied
+ on top of `regions` and of the account's enabled regions. Also
+ settable via the PROWLER_AWS_DISALLOWED_REGIONS environment variable
+ or the `disallowed_regions` key in the provider config file.
- organizations_role_arn: The ARN of the AWS Organizations IAM role to assume.
- scan_unused_services: A boolean indicating whether to scan unused services. False by default.
- resource_tags: A list of tags to filter the resources to audit.
@@ -190,6 +195,33 @@ class AwsProvider(Provider):
logger.info("Initializing AWS provider ...")
+ # Load provider config early because provider-level settings can affect
+ # bootstrap region selection before the scan starts.
+ if config_content is not None:
+ self._audit_config = config_content
+ else:
+ if not config_path:
+ config_path = default_config_file_path
+ self._audit_config = load_and_validate_config_file(self._type, config_path)
+
+ excluded_regions = self.resolve_excluded_regions(
+ excluded_regions, self._audit_config
+ )
+
+ # Normalize excluded_regions and prune the include-list up front so
+ # every downstream consumer (identity, STS region, service/region
+ # enumeration) sees an already-filtered view.
+ if excluded_regions and regions:
+ regions = set(regions) - excluded_regions
+ if not regions:
+ raise AWSArgumentTypeValidationError(
+ message=(
+ "All requested AWS regions are excluded by the "
+ "disallowed regions configuration."
+ ),
+ file=pathlib.Path(__file__).name,
+ )
+
######## AWS Session
logger.info("Generating original session ...")
@@ -215,7 +247,7 @@ class AwsProvider(Provider):
# After the session is created, validate it
logger.info("Validating credentials ...")
sts_region = get_aws_region_for_sts(
- self.session.current_session.region_name, regions
+ self.session.current_session.region_name, regions, excluded_regions
)
# Validate the credentials
@@ -229,7 +261,9 @@ class AwsProvider(Provider):
######## AWS Provider Identity
# Get profile region
- profile_region = self.get_profile_region(self._session.current_session)
+ profile_region = self.get_profile_region(
+ self._session.current_session, excluded_regions
+ )
# Set identity
self._identity = self.set_identity(
@@ -332,7 +366,26 @@ class AwsProvider(Provider):
)
########
- # Parse Scan Tags
+ # Get Enabled Regions
+ self._enabled_regions = self.get_aws_enabled_regions(
+ self._session.current_session
+ )
+
+ # Apply the exclusion to the account's enabled regions. This is the
+ # gate used by generate_regional_clients, so skipped regions never get
+ # a boto3 client created for them and cannot stall the scan.
+ if excluded_regions:
+ if self._enabled_regions is not None:
+ self._enabled_regions = self._enabled_regions - excluded_regions
+ if self._identity.audited_regions:
+ self._identity.audited_regions = (
+ set(self._identity.audited_regions) - excluded_regions
+ )
+ logger.info(f"Excluding AWS regions from scan: {sorted(excluded_regions)}")
+ self._excluded_regions = excluded_regions
+
+ # Parse Scan Tags after region exclusions are applied so tag discovery
+ # also skips disallowed regions.
if resource_tags:
self._audit_resources = self.get_tagged_resources(resource_tags)
@@ -340,22 +393,9 @@ class AwsProvider(Provider):
if resource_arn:
self._audit_resources = resource_arn
- # Get Enabled Regions
- self._enabled_regions = self.get_aws_enabled_regions(
- self._session.current_session
- )
-
# Set ignore unused services
self._scan_unused_services = scan_unused_services
- # Audit Config
- if config_content:
- self._audit_config = config_content
- else:
- if not config_path:
- config_path = default_config_file_path
- self._audit_config = load_and_validate_config_file(self._type, config_path)
-
# Fixer Config
self._fixer_config = fixer_config
@@ -468,12 +508,53 @@ class AwsProvider(Provider):
)
@staticmethod
- def get_profile_region(session: Session):
- profile_region = AWS_REGION_US_EAST_1
- if session.region_name:
- profile_region = session.region_name
+ def resolve_excluded_regions(
+ excluded_regions: set | list | tuple | None,
+ audit_config: dict | None,
+ ) -> set[str]:
+ """Resolve AWS region exclusions with precedence arg > env > config."""
+ if excluded_regions is not None:
+ raw_regions = excluded_regions
+ else:
+ raw_regions = Provider.get_excluded_regions_from_env()
+ if not raw_regions and isinstance(audit_config, dict):
+ raw_regions = audit_config.get("disallowed_regions") or []
- return profile_region
+ return {str(region).strip() for region in raw_regions if str(region).strip()}
+
+ @staticmethod
+ def get_bootstrap_region_candidates(session_region: str | None) -> tuple[str, ...]:
+ """Return safe fallback regions for bootstrap AWS calls."""
+ if session_region:
+ if session_region.startswith("cn-"):
+ return ("cn-north-1", "cn-northwest-1")
+ if session_region.startswith("us-gov-"):
+ return ("us-gov-east-1", "us-gov-west-1")
+ if session_region.startswith("eusc-"):
+ return ("eusc-de-east-1",)
+ if session_region.startswith("us-iso"):
+ return (session_region,)
+
+ return (AWS_STS_GLOBAL_ENDPOINT_REGION, "us-east-2", "us-west-2", "eu-west-1")
+
+ @staticmethod
+ def get_profile_region(
+ session: Session, excluded_regions: set[str] | None = None
+ ) -> str:
+ excluded_regions = set(excluded_regions or ())
+ session_region = session.region_name
+ if session_region and session_region not in excluded_regions:
+ return session_region
+
+ for region in AwsProvider.get_bootstrap_region_candidates(session_region):
+ if region not in excluded_regions:
+ if session_region and session_region != region:
+ logger.info(
+ f"Configured AWS profile region {session_region} is excluded; using {region} for bootstrap clients."
+ )
+ return region
+
+ return session_region or AWS_REGION_US_EAST_1
@staticmethod
def set_identity(
@@ -701,12 +782,15 @@ class AwsProvider(Provider):
Caller Identity ARN: arn:aws:iam::123456789012:user/prowler
```
"""
- # Beautify audited regions, set "all" if there is no filter region
- regions = (
- ", ".join(self._identity.audited_regions)
- if self._identity.audited_regions is not None
- else "all"
- )
+ # Beautify audited regions. If the scan includes all regions but some
+ # are explicitly excluded, reflect that in the banner instead of
+ # showing the misleading "all" label.
+ if self._identity.audited_regions:
+ regions = ", ".join(sorted(self._identity.audited_regions))
+ elif getattr(self, "_excluded_regions", None):
+ regions = f"all except {', '.join(sorted(self._excluded_regions))}"
+ else:
+ regions = "all"
# Beautify audited profile, set "default" if there is no profile set
profile = (
self._identity.profile if self._identity.profile is not None else "default"
@@ -745,6 +829,8 @@ class AwsProvider(Provider):
service_regions = AwsProvider.get_available_aws_service_regions(
service, self._identity.partition, self._identity.audited_regions
)
+ if getattr(self, "_excluded_regions", None):
+ service_regions = service_regions - self._excluded_regions
# Get the regions enabled for the account and get the intersection with the service available regions
if self._enabled_regions is not None:
@@ -962,6 +1048,8 @@ class AwsProvider(Provider):
service_regions = AwsProvider.get_available_aws_service_regions(
service, self._identity.partition, self._identity.audited_regions
)
+ if getattr(self, "_excluded_regions", None):
+ service_regions = service_regions - self._excluded_regions
default_region = self.get_global_region()
# global region of the partition when all regions are audited and there is no profile region
if self._identity.profile_region in service_regions:
@@ -1565,13 +1653,19 @@ def read_aws_regions_file() -> dict:
# TODO: This can be moved to another class since it doesn't need self
-def get_aws_region_for_sts(session_region: str, regions: set[str]) -> str:
+def get_aws_region_for_sts(
+ session_region: str,
+ regions: set[str],
+ excluded_regions: set[str] | None = None,
+) -> str:
"""
Get the AWS region for the STS Assume Role operation.
Args:
- session_region (str): The region configured in the AWS session.
- regions (set[str]): The regions passed with the -f/--region/--filter-region option.
+ - excluded_regions (set[str] | None): Regions that should be avoided for
+ bootstrap calls when possible.
Returns:
str: The AWS region for the STS Assume Role operation
@@ -1579,20 +1673,21 @@ def get_aws_region_for_sts(session_region: str, regions: set[str]) -> str:
Example:
aws_region = get_aws_region_for_sts(session_region, regions)
"""
- # If there is no region passed with -f/--region/--filter-region
- if regions is None or len(regions) == 0:
- # If you have a region configured in your AWS config or credentials file
- if session_region is not None:
- aws_region = session_region
- else:
- # If there is no region set passed with -f/--region
- # we use the Global STS Endpoint Region, us-east-1
- aws_region = AWS_STS_GLOBAL_ENDPOINT_REGION
- else:
- # Get the first region passed to the -f/--region
- aws_region = list(regions)[0]
+ excluded_regions = set(excluded_regions or ())
- return aws_region
+ if regions:
+ for region in regions:
+ if region not in excluded_regions:
+ return region
+
+ if session_region and session_region not in excluded_regions:
+ return session_region
+
+ for region in AwsProvider.get_bootstrap_region_candidates(session_region):
+ if region not in excluded_regions:
+ return region
+
+ return session_region or AWS_STS_GLOBAL_ENDPOINT_REGION
# TODO: this duplicates the provider arguments validation library
diff --git a/prowler/providers/aws/lib/arguments/arguments.py b/prowler/providers/aws/lib/arguments/arguments.py
index eb611c0d04..50f4665b2d 100644
--- a/prowler/providers/aws/lib/arguments/arguments.py
+++ b/prowler/providers/aws/lib/arguments/arguments.py
@@ -66,6 +66,16 @@ def init_parser(self):
help="AWS region names to run Prowler against",
choices=AwsProvider.get_regions(partition=None),
)
+ aws_regions_subparser.add_argument(
+ "--excluded-region",
+ "--excluded-regions",
+ nargs="+",
+ help=(
+ "AWS region names to exclude from the scan. Overrides the "
+ "PROWLER_AWS_DISALLOWED_REGIONS environment variable when set."
+ ),
+ choices=AwsProvider.get_regions(partition=None),
+ )
# AWS Organizations
aws_orgs_subparser = aws_parser.add_argument_group("AWS Organizations")
aws_orgs_subparser.add_argument(
diff --git a/prowler/providers/aws/lib/quick_inventory/quick_inventory.py b/prowler/providers/aws/lib/quick_inventory/quick_inventory.py
index 8e7bc6e450..95e9aea7bb 100644
--- a/prowler/providers/aws/lib/quick_inventory/quick_inventory.py
+++ b/prowler/providers/aws/lib/quick_inventory/quick_inventory.py
@@ -30,10 +30,12 @@ def quick_inventory(provider: AwsProvider, args):
ec2_client = provider.session.current_session.client(
"ec2", region_name=provider.identity.profile_region
)
+ excluded_regions = getattr(provider, "_excluded_regions", set())
# Get all the available regions
provider.identity.audited_regions = [
region["RegionName"]
for region in ec2_client.describe_regions()["Regions"]
+ if region["RegionName"] not in excluded_regions
]
with alive_bar(
diff --git a/prowler/providers/cloudflare/cloudflare_provider.py b/prowler/providers/cloudflare/cloudflare_provider.py
index 35dd88ef76..48763df395 100644
--- a/prowler/providers/cloudflare/cloudflare_provider.py
+++ b/prowler/providers/cloudflare/cloudflare_provider.py
@@ -274,8 +274,12 @@ class CloudflareProvider(Provider):
for account in client.accounts.list():
account_id = getattr(account, "id", None)
- # Prevent infinite loop - skip if we've seen this account
+ # Prevent infinite loop on repeated pages from the SDK paginator
if account_id in seen_account_ids:
+ logger.warning(
+ "Detected repeated Cloudflare account ID while listing accounts. "
+ "Stopping pagination to avoid an infinite loop."
+ )
break
seen_account_ids.add(account_id)
@@ -332,19 +336,16 @@ class CloudflareProvider(Provider):
return
except PermissionDeniedError as error:
error_str = str(error)
- # Check for user-level authentication required (code 9109)
- if "9109" in error_str:
- logger.error(f"CloudflareUserTokenRequiredError: {error}")
- raise CloudflareUserTokenRequiredError(
- file=os.path.basename(__file__),
- )
# Check for invalid API key or email (code 9103) - comes as 403
if "9103" in error_str or "Unknown X-Auth-Key" in error_str:
logger.error(f"CloudflareInvalidAPIKeyError: {error}")
raise CloudflareInvalidAPIKeyError(
file=os.path.basename(__file__),
)
- # For other permission errors, try accounts.list() as fallback
+ # For permission errors (including 9109 account-scoped tokens),
+ # try accounts.list() as fallback before failing.
+ # Error 9109 means the token is account-scoped, not user-level,
+ # which is valid for scanning — only fail if accounts.list() also fails.
logger.warning(
f"Unable to retrieve Cloudflare user info: {error}. "
"Trying accounts.list() as fallback."
@@ -398,7 +399,20 @@ class CloudflareProvider(Provider):
# Fallback: try accounts.list()
try:
- accounts = list(client.accounts.list())
+ accounts: list = []
+ seen_account_ids: set = set()
+ for account in client.accounts.list():
+ account_id = getattr(account, "id", None)
+ # Prevent infinite loop on repeated pages from the SDK paginator
+ if account_id in seen_account_ids:
+ logger.warning(
+ "Detected repeated Cloudflare account ID while validating credentials. "
+ "Stopping pagination to avoid an infinite loop."
+ )
+ break
+ seen_account_ids.add(account_id)
+ accounts.append(account)
+
if not accounts:
logger.error("CloudflareNoAccountsError: No accounts found")
raise CloudflareNoAccountsError(
diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py
index bc00607f10..8613957a68 100644
--- a/prowler/providers/common/provider.py
+++ b/prowler/providers/common/provider.py
@@ -1,5 +1,6 @@
import importlib
import importlib.metadata
+import os
import pkgutil
import sys
from abc import ABC, abstractmethod
@@ -199,6 +200,18 @@ class Provider(ABC):
# --- End dynamic provider contract methods ---
+ @staticmethod
+ def get_excluded_regions_from_env() -> set:
+ """Parse the PROWLER_AWS_DISALLOWED_REGIONS environment variable.
+
+ The variable is a comma-separated list of region identifiers to skip
+ during scans (e.g. "me-south-1, ap-east-1"). Whitespace around entries
+ is tolerated and empty entries are dropped. Returns an empty set when
+ the variable is unset or contains no usable values.
+ """
+ raw = os.environ.get("PROWLER_AWS_DISALLOWED_REGIONS", "")
+ return {region.strip() for region in raw.split(",") if region.strip()}
+
@staticmethod
def get_global_provider() -> "Provider":
return Provider._global
@@ -232,6 +245,11 @@ class Provider(ABC):
if not isinstance(Provider._global, provider_class):
if "aws" in provider_class_name.lower():
+ excluded_regions = (
+ set(arguments.excluded_region)
+ if getattr(arguments, "excluded_region", None)
+ else None
+ )
provider_class(
retries_max_attempts=arguments.aws_retries_max_attempts,
role_arn=arguments.role,
@@ -241,6 +259,7 @@ class Provider(ABC):
mfa=arguments.mfa,
profile=arguments.profile,
regions=set(arguments.region) if arguments.region else None,
+ excluded_regions=excluded_regions,
organizations_role_arn=arguments.organizations_role,
scan_unused_services=arguments.scan_unused_services,
resource_tags=arguments.resource_tag,
diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py
index 7af51cbfba..1da8769b06 100644
--- a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.py
@@ -35,21 +35,20 @@ class calendar_external_invitations_warning(Check):
f"External invitation warnings for Google Calendar are enabled "
f"in domain {calendar_client.provider.identity.domain}."
)
+ elif warning_enabled is None:
+ report.status = "PASS"
+ report.status_extended = (
+ f"External invitation warnings for Google Calendar use Google's "
+ f"secure default configuration (enabled) "
+ f"in domain {calendar_client.provider.identity.domain}."
+ )
else:
report.status = "FAIL"
- if warning_enabled is None:
- report.status_extended = (
- f"External invitation warnings for Google Calendar are not "
- f"explicitly configured in domain "
- f"{calendar_client.provider.identity.domain}. "
- f"Users should be warned when inviting guests outside the organization."
- )
- else:
- report.status_extended = (
- f"External invitation warnings for Google Calendar are disabled "
- f"in domain {calendar_client.provider.identity.domain}. "
- f"Users should be warned when inviting guests outside the organization."
- )
+ report.status_extended = (
+ f"External invitation warnings for Google Calendar are disabled "
+ f"in domain {calendar_client.provider.identity.domain}. "
+ f"Users should be warned when inviting guests outside the organization."
+ )
findings.append(report)
diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py
index b019acf4de..eaf9f90911 100644
--- a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py
+++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.py
@@ -36,20 +36,20 @@ class calendar_external_sharing_primary_calendar(Check):
f"{calendar_client.provider.identity.domain} is restricted to "
f"free/busy information only."
)
+ elif sharing is None:
+ report.status = "PASS"
+ report.status_extended = (
+ f"Primary calendar external sharing uses Google's secure default "
+ f"configuration (free/busy only) "
+ f"in domain {calendar_client.provider.identity.domain}."
+ )
else:
report.status = "FAIL"
- if sharing is None:
- report.status_extended = (
- f"Primary calendar external sharing is not explicitly configured "
- f"in domain {calendar_client.provider.identity.domain}. "
- f"External sharing should be restricted to free/busy information only."
- )
- else:
- report.status_extended = (
- f"Primary calendar external sharing in domain "
- f"{calendar_client.provider.identity.domain} is set to {sharing}. "
- f"External sharing should be restricted to free/busy information only."
- )
+ report.status_extended = (
+ f"Primary calendar external sharing in domain "
+ f"{calendar_client.provider.identity.domain} is set to {sharing}. "
+ f"External sharing should be restricted to free/busy information only."
+ )
findings.append(report)
diff --git a/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py b/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py
index 973a672f82..15f6187949 100644
--- a/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py
+++ b/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py
@@ -33,21 +33,20 @@ class drive_external_sharing_warn_users(Check):
f"External sharing warnings for Drive and Docs are enabled "
f"in domain {drive_client.provider.identity.domain}."
)
+ elif warning_enabled is None:
+ report.status = "PASS"
+ report.status_extended = (
+ f"External sharing warnings for Drive and Docs use Google's "
+ f"secure default configuration (enabled) "
+ f"in domain {drive_client.provider.identity.domain}."
+ )
else:
report.status = "FAIL"
- if warning_enabled is None:
- report.status_extended = (
- f"External sharing warnings for Drive and Docs are not "
- f"explicitly configured in domain "
- f"{drive_client.provider.identity.domain}. "
- f"Users should be warned when sharing files outside the organization."
- )
- else:
- report.status_extended = (
- f"External sharing warnings for Drive and Docs are disabled "
- f"in domain {drive_client.provider.identity.domain}. "
- f"Users should be warned when sharing files outside the organization."
- )
+ report.status_extended = (
+ f"External sharing warnings for Drive and Docs are disabled "
+ f"in domain {drive_client.provider.identity.domain}. "
+ f"Users should be warned when sharing files outside the organization."
+ )
findings.append(report)
diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py
index c36611326f..f976891147 100644
--- a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py
+++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py
@@ -35,22 +35,21 @@ class drive_shared_drive_creation_allowed(Check):
f"Users in domain {drive_client.provider.identity.domain} "
f"are allowed to create new shared drives."
)
+ elif allow_creation is None:
+ report.status = "PASS"
+ report.status_extended = (
+ f"Shared drive creation uses Google's secure default "
+ f"configuration (allowed) "
+ f"in domain {drive_client.provider.identity.domain}."
+ )
else:
report.status = "FAIL"
- if allow_creation is None:
- report.status_extended = (
- f"Shared drive creation is not explicitly configured in "
- f"domain {drive_client.provider.identity.domain}. "
- f"Users should be allowed to create new shared drives to avoid "
- f"data loss when accounts are deleted."
- )
- else:
- report.status_extended = (
- f"Users in domain {drive_client.provider.identity.domain} "
- f"are prevented from creating new shared drives. "
- f"Users should be allowed to create new shared drives to avoid "
- f"data loss when accounts are deleted."
- )
+ report.status_extended = (
+ f"Users in domain {drive_client.provider.identity.domain} "
+ f"are prevented from creating new shared drives. "
+ f"Users should be allowed to create new shared drives to avoid "
+ f"data loss when accounts are deleted."
+ )
findings.append(report)
diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py
index b112cd70b5..45edd64884 100644
--- a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py
+++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py
@@ -35,21 +35,21 @@ class drive_shared_drive_disable_download_print_copy(Check):
f"{drive_client.provider.identity.domain} is restricted to "
f"{allowed}."
)
+ elif allowed is None:
+ report.status = "PASS"
+ report.status_extended = (
+ f"Download, print, and copy restrictions for shared drives use "
+ f"Google's secure default configuration (disabled for viewers "
+ f"and commenters) "
+ f"in domain {drive_client.provider.identity.domain}."
+ )
else:
report.status = "FAIL"
- if allowed is None:
- report.status_extended = (
- f"Download, print, and copy restrictions for shared drive "
- f"viewers and commenters are not explicitly configured in "
- f"domain {drive_client.provider.identity.domain}. "
- f"These actions should be restricted to editors or managers only."
- )
- else:
- report.status_extended = (
- f"Download, print, and copy in shared drives in domain "
- f"{drive_client.provider.identity.domain} is set to {allowed}. "
- f"These actions should be restricted to editors or managers only."
- )
+ report.status_extended = (
+ f"Download, print, and copy in shared drives in domain "
+ f"{drive_client.provider.identity.domain} is set to {allowed}. "
+ f"These actions should be restricted to editors or managers only."
+ )
findings.append(report)
diff --git a/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py b/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py
index e5f243ae36..f8d8f4b586 100644
--- a/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py
+++ b/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py
@@ -36,21 +36,20 @@ class drive_warn_sharing_with_allowlisted_domains(Check):
f"Users are warned when sharing files with allowlisted "
f"domains in domain {drive_client.provider.identity.domain}."
)
+ elif warn_enabled is None:
+ report.status = "PASS"
+ report.status_extended = (
+ f"Warning when sharing with allowlisted domains uses Google's "
+ f"secure default configuration (enabled) "
+ f"in domain {drive_client.provider.identity.domain}."
+ )
else:
report.status = "FAIL"
- if warn_enabled is None:
- report.status_extended = (
- f"Warning when sharing with allowlisted domains is not "
- f"explicitly configured in domain "
- f"{drive_client.provider.identity.domain}. "
- f"Users should be warned when sharing files with users in allowlisted domains."
- )
- else:
- report.status_extended = (
- f"Warning when sharing with allowlisted domains is disabled "
- f"in domain {drive_client.provider.identity.domain}. "
- f"Users should be warned when sharing files with users in allowlisted domains."
- )
+ report.status_extended = (
+ f"Warning when sharing with allowlisted domains is disabled "
+ f"in domain {drive_client.provider.identity.domain}. "
+ f"Users should be warned when sharing files with users in allowlisted domains."
+ )
findings.append(report)
diff --git a/prowler/providers/image/image_provider.py b/prowler/providers/image/image_provider.py
index d724542e28..7c9a9cbb08 100644
--- a/prowler/providers/image/image_provider.py
+++ b/prowler/providers/image/image_provider.py
@@ -163,42 +163,50 @@ class ImageProvider(Provider):
# Registry scan mode: enumerate images from registry
if self.registry:
self._enumerate_registry()
- if self._listing_only:
- return
- for image in self.images:
- self._validate_image_name(image)
-
- if not self.images:
- raise ImageNoImagesProvidedError(
- file=__file__,
- message="No images provided for scanning.",
- )
-
- # Audit Config
- if config_content:
- self._audit_config = config_content
- else:
- if not config_path:
- config_path = default_config_file_path
- self._audit_config = load_and_validate_config_file(self._type, config_path)
-
- # Fixer Config
- self._fixer_config = fixer_config if fixer_config is not None else {}
-
- # Mutelist (not needed for Image provider since Trivy has its own logic)
+ # Safe defaults for listing-only mode (overwritten below in scan mode)
+ self._audit_config = {}
+ self._fixer_config = {}
self._mutelist = None
+ self.audit_metadata = None
- self.audit_metadata = Audit_Metadata(
- provider=self._type,
- account_id=self.audited_account,
- account_name="image",
- region=self.region,
- services_scanned=0,
- expected_checks=[],
- completed_checks=0,
- audit_progress=0,
- )
+ # Skip scan setup for listing-only mode
+ if not self._listing_only:
+ for image in self.images:
+ self._validate_image_name(image)
+
+ if not self.images:
+ raise ImageNoImagesProvidedError(
+ file=__file__,
+ message="No images provided for scanning.",
+ )
+
+ # Audit Config
+ if config_content:
+ self._audit_config = config_content
+ else:
+ if not config_path:
+ config_path = default_config_file_path
+ self._audit_config = load_and_validate_config_file(
+ self._type, config_path
+ )
+
+ # Fixer Config
+ self._fixer_config = fixer_config if fixer_config is not None else {}
+
+ # Mutelist (not needed for Image provider since Trivy has its own logic)
+ self._mutelist = None
+
+ self.audit_metadata = Audit_Metadata(
+ provider=self._type,
+ account_id=self.audited_account,
+ account_name="image",
+ region=self.region,
+ services_scanned=0,
+ expected_checks=[],
+ completed_checks=0,
+ audit_progress=0,
+ )
Provider.set_global_provider(self)
diff --git a/prowler/providers/vercel/services/project/project_service.py b/prowler/providers/vercel/services/project/project_service.py
index 9b3b24859d..e139a2a939 100644
--- a/prowler/providers/vercel/services/project/project_service.py
+++ b/prowler/providers/vercel/services/project/project_service.py
@@ -55,6 +55,7 @@ class Project(VercelService):
# Parse password protection
pwd_protection = proj.get("passwordProtection")
+ security = proj.get("security", {}) or {}
self.projects[project_id] = VercelProject(
id=project_id,
@@ -75,6 +76,16 @@ class Project(VercelService):
git_fork_protection=proj.get("gitForkProtection", True),
git_repository=proj.get("link"),
secure_compute=proj.get("secureCompute"),
+ firewall_enabled=security.get("firewallEnabled"),
+ firewall_config_version=(
+ str(security.get("firewallConfigVersion"))
+ if security.get("firewallConfigVersion") is not None
+ else None
+ ),
+ managed_rules=security.get(
+ "managedRules", security.get("managedRulesets")
+ ),
+ bot_id_enabled=security.get("botIdEnabled"),
)
logger.info(f"Project - Found {len(self.projects)} project(s)")
@@ -160,4 +171,8 @@ class VercelProject(BaseModel):
git_fork_protection: bool = True
git_repository: Optional[dict] = None
secure_compute: Optional[dict] = None
+ firewall_enabled: Optional[bool] = None
+ firewall_config_version: Optional[str] = None
+ managed_rules: Optional[dict] = None
+ bot_id_enabled: Optional[bool] = None
environment_variables: list[VercelEnvironmentVariable] = Field(default_factory=list)
diff --git a/prowler/providers/vercel/services/security/security_service.py b/prowler/providers/vercel/services/security/security_service.py
index eadac856b9..3c79f5c2f5 100644
--- a/prowler/providers/vercel/services/security/security_service.py
+++ b/prowler/providers/vercel/services/security/security_service.py
@@ -26,10 +26,7 @@ class Security(VercelService):
def _fetch_firewall_config(self, project):
"""Fetch WAF/Firewall config for a single project."""
try:
- data = self._get(
- "/v1/security/firewall/config",
- params={"projectId": project.id},
- )
+ data = self._read_firewall_config(project)
if data is None:
# 403 — plan limitation, store with managed_rulesets=None
@@ -44,39 +41,60 @@ class Security(VercelService):
)
return
- # Parse firewall config
- fw = data.get("firewallConfig", data) if isinstance(data, dict) else {}
+ fw = self._normalize_firewall_config(data)
- # Determine if firewall is enabled
- rules = fw.get("rules", []) or []
- managed = fw.get("managedRules", fw.get("managedRulesets"))
+ if not fw:
+ fallback_firewall_enabled = self._fallback_firewall_enabled(project)
+ self.firewall_configs[project.id] = VercelFirewallConfig(
+ project_id=project.id,
+ project_name=project.name,
+ team_id=project.team_id,
+ firewall_enabled=(
+ fallback_firewall_enabled
+ if fallback_firewall_enabled is not None
+ else False
+ ),
+ managed_rulesets=self._fallback_managed_rulesets(project),
+ name=project.name,
+ id=project.id,
+ )
+ return
+
+ rules = [
+ rule for rule in (fw.get("rules", []) or []) if self._is_active(rule)
+ ]
+ managed = self._active_managed_rulesets(
+ fw.get("managedRules", fw.get("managedRulesets", fw.get("crs")))
+ )
custom_rules = []
- ip_blocking = []
+ ip_blocking = list(fw.get("ips", []) or [])
rate_limiting = []
for rule in rules:
- rule_action = rule.get("action", {})
- action_type = (
- rule_action.get("type", "")
- if isinstance(rule_action, dict)
- else str(rule_action)
- )
+ mitigate_action = self._mitigate_action(rule)
- if action_type == "rate_limit" or rule.get("rateLimit"):
+ if self._is_rate_limiting_rule(rule, mitigate_action):
rate_limiting.append(rule)
- elif action_type in ("deny", "block") and self._is_ip_rule(rule):
+ elif self._is_ip_rule(rule):
ip_blocking.append(rule)
else:
custom_rules.append(rule)
- firewall_enabled = bool(rules) or bool(managed)
+ firewall_enabled = fw.get("firewallEnabled")
+ if firewall_enabled is None:
+ firewall_enabled = self._fallback_firewall_enabled(project)
+ if firewall_enabled is None:
+ firewall_enabled = bool(rules) or bool(ip_blocking) or bool(managed)
+
+ if not managed:
+ managed = self._fallback_managed_rulesets(project)
self.firewall_configs[project.id] = VercelFirewallConfig(
project_id=project.id,
project_name=project.name,
team_id=project.team_id,
firewall_enabled=firewall_enabled,
- managed_rulesets=managed if managed is not None else {},
+ managed_rulesets=managed,
custom_rules=custom_rules,
ip_blocking_rules=ip_blocking,
rate_limiting_rules=rate_limiting,
@@ -95,6 +113,117 @@ class Security(VercelService):
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
+ def _read_firewall_config(self, project):
+ """Read the deployed firewall config via the documented endpoint.
+
+ See: https://vercel.com/docs/rest-api/security/read-firewall-configuration
+ """
+ params = self._firewall_params(project)
+ config_version = getattr(project, "firewall_config_version", None)
+
+ endpoints = []
+ if config_version:
+ endpoints.append(f"/v1/security/firewall/config/{config_version}")
+ endpoints.append("/v1/security/firewall/config/active")
+
+ last_error = None
+ for endpoint in endpoints:
+ try:
+ return self._get(endpoint, params=params)
+ except Exception as error:
+ last_error = error
+ logger.warning(
+ f"Security - Firewall config read failed for project "
+ f"{project.id} (team={getattr(project, 'team_id', None)}) "
+ f"on {endpoint} with params={params}: "
+ f"{error.__class__.__name__}: {error}"
+ )
+
+ if last_error is not None:
+ logger.debug(
+ f"Security - Falling back to firewall config wrapper for "
+ f"{project.id} after {last_error.__class__.__name__}: {last_error}"
+ )
+
+ return self._get("/v1/security/firewall/config", params=params)
+
+ @staticmethod
+ def _firewall_params(project) -> dict:
+ """Build firewall request params, preserving team scope for team projects."""
+ params = {"projectId": project.id}
+ team_id = getattr(project, "team_id", None)
+
+ if isinstance(team_id, str) and team_id:
+ params["teamId"] = team_id
+
+ return params
+
+ @staticmethod
+ def _normalize_firewall_config(data: dict) -> dict:
+ """Normalize firewall responses across Vercel endpoint variants."""
+ if not isinstance(data, dict):
+ return {}
+
+ if "firewallConfig" in data and isinstance(data["firewallConfig"], dict):
+ return data["firewallConfig"]
+
+ if any(key in data for key in ("active", "draft", "versions")):
+ return data.get("active") or {}
+
+ return data
+
+ @staticmethod
+ def _active_managed_rulesets(managed_rules: dict | None) -> dict:
+ """Return only active managed rulesets."""
+ if not isinstance(managed_rules, dict):
+ return {}
+
+ return {
+ ruleset: config
+ for ruleset, config in managed_rules.items()
+ if not isinstance(config, dict) or config.get("active", False)
+ }
+
+ @classmethod
+ def _fallback_managed_rulesets(cls, project) -> dict:
+ """Return active managed rulesets from project metadata."""
+ return cls._active_managed_rulesets(getattr(project, "managed_rules", None))
+
+ @staticmethod
+ def _fallback_firewall_enabled(project) -> bool | None:
+ """Return firewall enabled state from project metadata when available."""
+ return getattr(project, "firewall_enabled", None)
+
+ @staticmethod
+ def _mitigate_action(rule: dict) -> dict:
+ """Extract the nested Vercel mitigation action payload for a rule."""
+ action = rule.get("action", {})
+ if not isinstance(action, dict):
+ return {}
+
+ mitigate = action.get("mitigate")
+ return mitigate if isinstance(mitigate, dict) else action
+
+ @staticmethod
+ def _is_active(rule: dict) -> bool:
+ """Treat missing active flags as enabled for backwards compatibility."""
+ return rule.get("active", True) is not False
+
+ @classmethod
+ def _is_rate_limiting_rule(
+ cls, rule: dict, mitigate_action: dict | None = None
+ ) -> bool:
+ """Check if a firewall rule enforces rate limiting."""
+ if rule.get("rateLimit"):
+ return True
+
+ mitigate = (
+ mitigate_action
+ if isinstance(mitigate_action, dict)
+ else cls._mitigate_action(rule)
+ )
+ return bool(mitigate.get("rateLimit")) or mitigate.get("action") == "rate_limit"
+
@staticmethod
def _is_ip_rule(rule: dict) -> bool:
"""Check if a rule is an IP blocking rule based on conditions."""
diff --git a/pyproject.toml b/pyproject.toml
index 0b4294a547..717fb338a7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,7 +30,7 @@ dependencies = [
"azure-mgmt-postgresqlflexibleservers==1.1.0",
"azure-mgmt-recoveryservices==3.1.0",
"azure-mgmt-recoveryservicesbackup==9.2.0",
- "azure-mgmt-resource==23.3.0",
+ "azure-mgmt-resource==24.0.0",
"azure-mgmt-search==9.1.0",
"azure-mgmt-security==7.0.0",
"azure-mgmt-sql==3.0.1",
@@ -57,7 +57,7 @@ dependencies = [
"kubernetes==32.0.1",
"markdown==3.10.2",
"microsoft-kiota-abstractions==1.9.2",
- "msgraph-sdk==1.23.0",
+ "msgraph-sdk==1.55.0",
"numpy==2.0.2",
"openstacksdk==4.2.0",
"pandas==2.2.3",
@@ -95,7 +95,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
name = "prowler"
readme = "README.md"
requires-python = ">=3.10,<3.13"
-version = "5.24.0"
+version = "5.25.0"
[project.scripts]
prowler = "prowler.__main__:prowler"
@@ -121,7 +121,6 @@ docker = "7.1.0"
filelock = "3.20.3"
flake8 = "7.1.2"
freezegun = "1.5.1"
-marshmallow = "==3.26.2"
mock = "5.2.0"
moto = {extras = ["all"], version = "5.1.11"}
openapi-schema-validator = "0.6.3"
diff --git a/skills/prowler-compliance/SKILL.md b/skills/prowler-compliance/SKILL.md
index 1853d23d8b..51c68eb05f 100644
--- a/skills/prowler-compliance/SKILL.md
+++ b/skills/prowler-compliance/SKILL.md
@@ -1,16 +1,28 @@
---
name: prowler-compliance
description: >
- Creates and manages Prowler compliance frameworks.
- Trigger: When working with compliance frameworks (CIS, NIST, PCI-DSS, SOC2, GDPR, ISO27001, ENS, MITRE ATT&CK).
+ Creates, syncs, audits and manages Prowler compliance frameworks end-to-end.
+ Covers the four-layer architecture (SDK models → JSON catalogs → output
+ formatters → API/UI), upstream sync workflows, cloud-auditor check-mapping
+ reviews, output formatter creation, and framework-specific attribute models.
+ Trigger: When working with compliance frameworks (CIS, NIST, PCI-DSS, SOC2,
+ GDPR, ISO27001, ENS, MITRE ATT&CK, CCC, C5, CSA CCM, KISA ISMS-P,
+ Prowler ThreatScore, FedRAMP, HIPAA), syncing with upstream catalogs,
+ auditing check-to-requirement mappings, adding output formatters, or fixing
+ compliance JSON bugs (duplicate IDs, empty Version, wrong Section, stale
+ check refs).
license: Apache-2.0
metadata:
author: prowler-cloud
- version: "1.1"
+ version: "1.2"
scope: [root, sdk]
auto_invoke:
- "Creating/updating compliance frameworks"
- "Mapping checks to compliance controls"
+ - "Syncing compliance framework with upstream catalog"
+ - "Auditing check-to-requirement mappings as a cloud auditor"
+ - "Adding a compliance output formatter (per-provider class + table dispatcher)"
+ - "Fixing compliance JSON bugs (duplicate IDs, empty Section, stale refs)"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
---
@@ -18,10 +30,82 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
Use this skill when:
- Creating a new compliance framework for any provider
+- **Syncing an existing framework with an upstream source of truth** (CIS, FINOS CCC, CSA CCM, NIST, ENS, etc.)
- Adding requirements to existing frameworks
- Mapping checks to compliance controls
+- **Auditing existing check mappings as a cloud auditor** (user asks "are these mappings correct?", "which checks apply to this requirement?", "review the mappings")
+- **Adding a new output formatter** (new framework needs a table dispatcher + per-provider classes + CSV models)
+- **Fixing JSON bugs**: duplicate IDs, empty Version, wrong Section, stale check refs, inconsistent FamilyName, padded tangential check mappings
+- **Registering a framework in the CLI table dispatcher or API export map**
+- Investigating why a finding/check isn't showing under the expected compliance framework in the UI
- Understanding compliance framework structures and attributes
+## Four-Layer Architecture (Mental Model)
+
+Prowler compliance is a **four-layer system** hanging off one Pydantic model tree. Bugs usually happen where one layer doesn't match another, so know all four before touching anything.
+
+### Layer 1: SDK / Core Models — `prowler/lib/check/`
+
+- **`compliance_models.py`** — Pydantic **v1** model tree (`from pydantic.v1 import`). One `*_Requirement_Attribute` class per framework type + `Generic_Compliance_Requirement_Attribute` as fallback.
+- `Compliance_Requirement.Attributes: list[Union[...]]` — **`Generic_Compliance_Requirement_Attribute` MUST be LAST** in the Union or every framework-specific attribute falls through to Generic (Pydantic v1 tries union members in order).
+- **`compliance.py`** — runtime linker. `get_check_compliance()` builds the key as `f"{Framework}-{Version}"` **only if `Version` is non-empty**. An empty Version makes the key just `"{Framework}"` — this breaks downstream filters and tests that expect the versioned key.
+- `Compliance.get_bulk(provider)` walks `prowler/compliance/{provider}/` and parses every `.json` file. No central index — just directory scan.
+
+### Layer 2: JSON Frameworks — `prowler/compliance/{provider}/`
+
+See "Compliance Framework Location" and "Framework-Specific Attribute Structures" sections below.
+
+### Layer 3: Output Formatters — `prowler/lib/outputs/compliance/{framework}/`
+
+**Every framework directory follows this exact convention** — do not deviate:
+
+```
+{framework}/
+├── __init__.py
+├── {framework}.py # ONLY get_{framework}_table() — NO function docstring
+├── {framework}_{provider}.py # One class per provider (e.g., CCC_AWS, CCC_Azure, CCC_GCP)
+└── models.py # One Pydantic v2 BaseModel per provider (CSV columns)
+```
+
+- **`{framework}.py`** holds the **table dispatcher function** `get_{framework}_table()`. It prints the pass/fail/muted summary table. **Must NOT import `Finding` or `ComplianceOutput`** — doing so creates a circular import with `prowler/lib/outputs/compliance/compliance.py`. Only imports: `colorama`, `tabulate`, `prowler.config.config.orange_color`.
+- **`{framework}_{provider}.py`** holds a per-provider class like `CCC_AWS(ComplianceOutput)` with a `transform()` method that walks findings and emits rows. This file IS allowed to import `Finding` because it's not on the dispatcher import chain.
+- **`models.py`** holds one Pydantic v2 `BaseModel` per provider. Field names become CSV column headers (**public API** — renaming breaks downstream consumers).
+- **Never collapse per-provider files into a unified parameterized class**, even when DRY-tempting. Every framework in Prowler follows the per-provider file pattern and reviewers will reject the refactor. CSV columns differ per provider (`AccountId`/`Region` vs `SubscriptionId`/`Location` vs `ProjectId`/`Location`) — three classes is the convention.
+- **No function docstring on `get_{framework}_table()`** — no other framework has one; stay consistent.
+- Register in `prowler/lib/outputs/compliance/compliance.py` → `display_compliance_table()` with an `elif compliance_framework.startswith("{framework}_"):` branch. Import the table function at the top of the file.
+
+### Layer 4: API / UI
+
+- **API table dispatcher**: `api/src/backend/tasks/jobs/export.py` → `COMPLIANCE_CLASS_MAP` keyed by provider. Uses `startswith` predicates: `(lambda name: name.startswith("ccc_"), CCC_AWS)`. **Never use exact match** (`name == "ccc_aws"`) — it's inconsistent and breaks versioning.
+- **API lazy loader**: `api/src/backend/api/compliance.py` — `LazyComplianceTemplate` and `LazyChecksMapping` load compliance per provider on first access.
+- **UI mapper routing**: `ui/lib/compliance/compliance-mapper.ts` routes framework names → per-framework mapper.
+- **UI per-framework mapper**: `ui/lib/compliance/{framework}.tsx` flattens `Requirements` into a 3-level tree (Framework → Category → Control → Requirement) for the accordion view. Groups by `Attributes[0].FamilyName` and `Attributes[0].Section`.
+- **UI detail panel**: `ui/components/compliance/compliance-custom-details/{framework}-details.tsx`.
+- **UI types**: `ui/types/compliance.ts` — TypeScript mirrors of the attribute metadata.
+
+### The CLI Pipeline (end-to-end)
+
+```
+prowler aws --compliance ccc_aws
+ ↓
+Compliance.get_bulk("aws") → parses prowler/compliance/aws/*.json
+ ↓
+update_checks_metadata_with_compliance() → attaches compliance info to CheckMetadata
+ ↓
+execute_checks() → runs checks, produces Finding objects
+ ↓
+get_check_compliance(finding, "aws", bulk_checks_metadata)
+ → dict "{Framework}-{Version}" → [requirement_ids]
+ ↓
+CCC_AWS(findings, compliance).transform() → per-provider class builds CSV rows
+ ↓
+batch_write_data_to_file() → writes {output_filename}_ccc_aws.csv
+ ↓
+display_compliance_table() → get_ccc_table() → prints stdout summary
+```
+
+---
+
## Compliance Framework Location
Frameworks are JSON files located in: `prowler/compliance/{provider}/{framework_name}_{provider}.json`
@@ -455,14 +539,453 @@ Prowler ThreatScore is a custom security scoring framework developed by Prowler
- **M365:** `cis_4.0_m365.json`, `iso27001_2022_m365.json`
- **NHN:** `iso27001_2022_nhn.json`
+## Workflow A: Sync a Framework With an Upstream Catalog
+
+Use when the framework is maintained upstream (CIS Benchmarks, FINOS CCC, CSA CCM, NIST, ENS, etc.) and Prowler needs to catch up.
+
+### Step 1 — Cache the upstream source
+
+Download every upstream file to a local cache so subsequent iterations don't hit the network. For FINOS CCC:
+
+```bash
+mkdir -p /tmp/ccc_upstream
+catalogs="core/ccc storage/object management/auditlog management/logging ..."
+for p in $catalogs; do
+ safe=$(echo "$p" | tr '/' '_')
+ gh api "repos/finos/common-cloud-controls/contents/catalogs/$p/controls.yaml" \
+ -H "Accept: application/vnd.github.raw" > "/tmp/ccc_upstream/${safe}.yaml"
+done
+```
+
+### Step 2 — Run the generic sync runner against a framework config
+
+The sync tooling is split into three layers so adding a new framework only takes a YAML config (and optionally a new parser module for an unfamiliar upstream format):
+
+```
+skills/prowler-compliance/assets/
+├── sync_framework.py # generic runner — works for any framework
+├── configs/
+│ └── ccc.yaml # per-framework config (canonical example)
+└── parsers/
+ ├── __init__.py
+ └── finos_ccc.py # parser module for FINOS CCC YAML
+```
+
+**For frameworks that already have a config + parser** (today: FINOS CCC), run:
+
+```bash
+python skills/prowler-compliance/assets/sync_framework.py \
+ skills/prowler-compliance/assets/configs/ccc.yaml
+```
+
+The runner loads the config, validates it, dynamically imports the parser declared in `parser.module`, calls `parser.parse_upstream(config) -> list[dict]`, then applies generic post-processing (id uniqueness safety net, `FamilyName` normalization, legacy check-mapping preservation) and writes the provider JSONs.
+
+**To add a new framework sync**:
+
+1. **Write a config file** at `skills/prowler-compliance/assets/configs/{framework}.yaml`. See `configs/ccc.yaml` as the canonical example. Required top-level sections:
+ - `framework` — `name`, `display_name`, `version` (**never empty** — empty Version silently breaks `get_check_compliance()` key construction, so the runner refuses to start), `description_template` (accepts `{provider_display}`, `{provider_key}`, `{framework_name}`, `{framework_display}`, `{version}` placeholders).
+ - `providers` — list of `{key, display}` pairs, one per Prowler provider the framework targets.
+ - `output.path_template` — supports `{provider}`, `{framework}`, `{version}` placeholders. Examples: `"prowler/compliance/{provider}/ccc_{provider}.json"` for unversioned file names, `"prowler/compliance/{provider}/cis_{version}_{provider}.json"` for versioned ones.
+ - `upstream.dir` — local cache directory (populate via Step 1).
+ - `parser.module` — name of the module under `parsers/` to load (without `.py`). Everything else under `parser.` is opaque to the runner and passed to the parser as config.
+ - `post_processing.check_preservation.primary_key` — top-level field name for the primary legacy-mapping lookup (almost always `Id`).
+ - `post_processing.check_preservation.fallback_keys` — **config-driven fallback keys** for preserving check mappings when ids change. Each entry is a list of `Attributes[0]` field names composed into a tuple. Examples:
+ - CCC: `- [Section, Applicability]` (because `Applicability` is a CCC-only attribute, verified in `compliance_models.py:213`).
+ - CIS would use `- [Section, Profile]`.
+ - NIST would use `- [ItemId]`.
+ - List-valued fields (like `Applicability`) are automatically frozen to `frozenset` so the tuple is hashable.
+ - `post_processing.family_name_normalization` (optional) — map of raw → canonical `FamilyName` values. The UI groups by `Attributes[0].FamilyName` exactly, so inconsistent upstream variants otherwise become separate tree branches.
+
+2. **Reuse an existing parser** if the upstream format matches one (currently only `finos_ccc` exists). Otherwise, **write a new parser** at `parsers/{name}.py` implementing:
+
+ ```python
+ def parse_upstream(config: dict) -> list[dict]:
+ """Return Prowler-format requirements {Id, Description, Attributes: [...], Checks: []}.
+
+ Ids MUST be unique in the returned list. The runner raises ValueError
+ on duplicates — it does NOT silently renumber, because mutating a
+ canonical upstream id (e.g. CIS '1.1.1' or NIST 'AC-2(1)') would be
+ catastrophic. The parser owns all upstream-format quirks: foreign-prefix
+ rewriting, genuine collision renumbering, shape handling.
+ """
+ ```
+
+ The parser reads its own settings from `config['upstream']` and `config['parser']`. It does NOT load existing Prowler JSONs (the runner does that for check preservation) and does NOT write output (the runner does that too).
+
+**Gotchas the runner already handles for you** (learned from the FINOS CCC v2025.10 sync — they're documented here so you don't re-discover them):
+
+- **Multiple upstream YAML shapes**. Most FINOS CCC catalogs use `control-families: [...]`, but `storage/object` uses a top-level `controls: [...]` with a `family: "CCC.X.Y"` reference id and no human-readable family name. A parser that only handles shape 1 silently drops the shape-2 catalog — this exact bug dropped ObjStor from Prowler for a full iteration. `parsers/finos_ccc.py` handles both shapes; if you write a new parser for a similar format, test with at least one file of each shape.
+- **Whitespace collapse**. Upstream YAML multi-line block scalars (`|`) preserve newlines. Prowler stores descriptions single-line. Collapse with `" ".join(value.split())` before emitting (see `parsers/finos_ccc.py::clean()`).
+- **Foreign-prefix AR id rewriting**. Upstream sometimes aliases requirements across catalogs by keeping the original prefix (e.g., `CCC.AuditLog.CN08.AR01` appears nested under `CCC.Logging.CN03`). Rewrite the foreign id to fit its parent control: `CCC.Logging.CN03.AR01`. This logic is parser-specific because the id structure varies per framework (CCC uses 3-dot depth; CIS uses numeric dots; NIST uses `AC-2(1)`).
+- **Genuine upstream collision renumbering**. Sometimes upstream has a real typo where two different requirements share the same id (e.g., `CCC.Core.CN14.AR02` defined twice for 30-day and 14-day backup variants). Renumber the second copy to the next free AR number (`.AR03`). The parser handles this; the runner asserts the final list has unique ids as a safety net.
+- **Existing check mapping preservation**. The runner uses the `primary_key` + `fallback_keys` declared in config to look up the old `Checks` list for each requirement. For CCC this means primary index by `Id` plus fallback index by `(Section, frozenset(Applicability))` — the fallback recovers mappings for requirements whose ids were rewritten or renumbered by the parser.
+- **FamilyName normalization**. Configured via `post_processing.family_name_normalization` — no code changes needed to collapse upstream variants like `"Logging & Monitoring"` → `"Logging and Monitoring"`.
+- **Populate `Version`**. The runner refuses to start on empty `framework.version` — fail-fast replaces the silent bug where `get_check_compliance()` would build the key as just `"{Framework}"`.
+
+### Step 3 — Validate before committing
+
+```python
+from prowler.lib.check.compliance_models import Compliance
+for prov in ['aws', 'azure', 'gcp']:
+ c = Compliance.parse_file(f"prowler/compliance/{prov}/ccc_{prov}.json")
+ print(f"{prov}: {len(c.Requirements)} reqs, version={c.Version}")
+```
+
+Any `ValidationError` means the Attribute fields don't match the `*_Requirement_Attribute` model. Either fix the JSON or extend the model in `compliance_models.py` (remember: Generic stays last).
+
+### Step 4 — Verify every check id exists
+
+```python
+import json
+from pathlib import Path
+for prov in ['aws', 'azure', 'gcp']:
+ existing = {p.stem.replace('.metadata','')
+ for p in Path(f'prowler/providers/{prov}/services').rglob('*.metadata.json')}
+ with open(f'prowler/compliance/{prov}/ccc_{prov}.json') as f:
+ data = json.load(f)
+ refs = {c for r in data['Requirements'] for c in r['Checks']}
+ missing = refs - existing
+ assert not missing, f"{prov} missing: {missing}"
+```
+
+A stale check id silently becomes dead weight — no finding will ever map to it. This pre-validation **must run on every write**; bake it into the generator script.
+
+### Step 5 — Add an attribute model if needed
+
+Only if the framework has fields beyond `Generic_Compliance_Requirement_Attribute`. Add the class to `prowler/lib/check/compliance_models.py` and register it in `Compliance_Requirement.Attributes: list[Union[...]]`. **Generic stays last.**
+
+---
+
+## Workflow B: Audit Check Mappings as a Cloud Auditor
+
+Use when the user asks to review existing mappings ("are these correct?", "verify that the checks apply", "audit the CCC mappings"). This is the highest-value compliance task — it surfaces padded mappings with zero actual coverage and missing mappings for legitimate coverage.
+
+### The golden rule
+
+> A Prowler check's title/risk MUST **literally describe what the requirement text says**. "Related" is not enough. If no check actually addresses the requirement, leave `Checks: []` (MANUAL) — **honest MANUAL is worth more than padded coverage**.
+
+### Audit process
+
+**Step 1 — Build a per-provider check inventory** (cache in `/tmp/`):
+
+```python
+import json
+from pathlib import Path
+for provider in ['aws', 'azure', 'gcp']:
+ inv = {}
+ for meta in Path(f'prowler/providers/{provider}/services').rglob('*.metadata.json'):
+ with open(meta) as f:
+ d = json.load(f)
+ cid = d.get('CheckID') or meta.stem.replace('.metadata','')
+ inv[cid] = {
+ 'service': d.get('ServiceName', ''),
+ 'title': d.get('CheckTitle', ''),
+ 'risk': d.get('Risk', ''),
+ 'description': d.get('Description', ''),
+ }
+ with open(f'/tmp/checks_{provider}.json', 'w') as f:
+ json.dump(inv, f, indent=2)
+```
+
+**Step 2 — Keyword/service query helper** — see [assets/query_checks.py](assets/query_checks.py):
+
+```bash
+python assets/query_checks.py aws encryption transit # keyword AND-search
+python assets/query_checks.py aws --service iam # all iam checks
+python assets/query_checks.py aws --id kms_cmk_rotation_enabled # full metadata
+```
+
+**Step 3 — Dump a framework section with current mappings** — see [assets/dump_section.py](assets/dump_section.py):
+
+```bash
+python assets/dump_section.py ccc "CCC.Core." # all Core ARs across 3 providers
+python assets/dump_section.py ccc "CCC.AuditLog." # all AuditLog ARs
+```
+
+**Step 4 — Encode explicit REPLACE decisions** — see [assets/audit_framework_template.py](assets/audit_framework_template.py). Structure:
+
+```python
+DECISIONS = {}
+
+DECISIONS["CCC.Core.CN01.AR01"] = {
+ "aws": [
+ "cloudfront_distributions_https_enabled",
+ "cloudfront_distributions_origin_traffic_encrypted",
+ # ...
+ ],
+ "azure": [
+ "storage_secure_transfer_required_is_enabled",
+ "app_minimum_tls_version_12",
+ # ...
+ ],
+ "gcp": [
+ "cloudsql_instance_ssl_connections",
+ ],
+ # Missing provider key = leave the legacy mapping untouched
+}
+
+# Empty list = EXPLICITLY MANUAL (overwrites legacy)
+DECISIONS["CCC.Core.CN01.AR07"] = {
+ "aws": [], # Prowler has no IANA port/protocol check
+ "azure": [],
+ "gcp": [],
+}
+```
+
+**REPLACE, not PATCH.** Encoding every mapping as a full list (not add/remove delta) makes the audit reproducible and surfaces hidden assumptions from the legacy data.
+
+**Step 5 — Pre-validation**. The audit script MUST validate every check id against the inventory and **abort with stderr listing typos**. Common typos caught during a real audit:
+
+- `fsx_file_system_encryption_at_rest_using_kms` (doesn't exist)
+- `cosmosdb_account_encryption_at_rest_with_cmk` (doesn't exist)
+- `sqlserver_geo_replication` (doesn't exist)
+- `redshift_cluster_audit_logging` (should be `redshift_cluster_encrypted_at_rest`)
+- `postgresql_flexible_server_require_secure_transport` (should be `postgresql_flexible_server_enforce_ssl_enabled`)
+- `storage_secure_transfer_required_enabled` (should be `storage_secure_transfer_required_is_enabled`)
+- `sqlserver_minimum_tls_version_12` (should be `sqlserver_recommended_minimal_tls_version`)
+
+**Step 6 — Apply + validate + test**:
+
+```bash
+python /path/to/audit_script.py # applies decisions, pre-validates
+python -m pytest tests/lib/outputs/compliance/ tests/lib/check/ -q
+```
+
+### Audit Reference Table: Requirement Text → Prowler Checks
+
+Use this table to map CCC-style / NIST-style / ISO-style requirements to the checks that actually verify them. Built from a real audit of 172 CCC ARs × 3 providers.
+
+| Requirement text | AWS checks | Azure checks | GCP checks |
+|---|---|---|---|
+| **TLS in transit enforced** | `cloudfront_distributions_https_enabled`, `s3_bucket_secure_transport_policy`, `elbv2_ssl_listeners`, `elbv2_insecure_ssl_ciphers`, `elb_ssl_listeners`, `elb_insecure_ssl_ciphers`, `opensearch_service_domains_https_communications_enforced`, `rds_instance_transport_encrypted`, `redshift_cluster_in_transit_encryption_enabled`, `elasticache_redis_cluster_in_transit_encryption_enabled`, `dynamodb_accelerator_cluster_in_transit_encryption_enabled`, `dms_endpoint_ssl_enabled`, `kafka_cluster_in_transit_encryption_enabled`, `transfer_server_in_transit_encryption_enabled`, `glue_database_connections_ssl_enabled`, `sns_subscription_not_using_http_endpoints` | `storage_secure_transfer_required_is_enabled`, `storage_ensure_minimum_tls_version_12`, `postgresql_flexible_server_enforce_ssl_enabled`, `mysql_flexible_server_ssl_connection_enabled`, `mysql_flexible_server_minimum_tls_version_12`, `sqlserver_recommended_minimal_tls_version`, `app_minimum_tls_version_12`, `app_ensure_http_is_redirected_to_https`, `app_ftp_deployment_disabled` | `cloudsql_instance_ssl_connections` (almost only option) |
+| **TLS 1.3 specifically** | Partial: `cloudfront_distributions_using_deprecated_ssl_protocols`, `elb*_insecure_ssl_ciphers`, `*_minimum_tls_version_12` | Partial: `*_minimum_tls_version_12` checks | None — accept as MANUAL |
+| **SSH / port 22 hardening** | `ec2_instance_port_ssh_exposed_to_internet`, `ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22`, `ec2_networkacl_allow_ingress_tcp_port_22` | `network_ssh_internet_access_restricted`, `vm_linux_enforce_ssh_authentication` | `compute_firewall_ssh_access_from_the_internet_allowed`, `compute_instance_block_project_wide_ssh_keys_disabled`, `compute_project_os_login_enabled`, `compute_project_os_login_2fa_enabled` |
+| **mTLS (mutual TLS)** | `kafka_cluster_mutual_tls_authentication_enabled`, `apigateway_restapi_client_certificate_enabled` | `app_client_certificates_on` | None — MANUAL |
+| **Data at rest encrypted** | `s3_bucket_default_encryption`, `s3_bucket_kms_encryption`, `ec2_ebs_default_encryption`, `ec2_ebs_volume_encryption`, `rds_instance_storage_encrypted`, `rds_cluster_storage_encrypted`, `rds_snapshots_encrypted`, `dynamodb_tables_kms_cmk_encryption_enabled`, `redshift_cluster_encrypted_at_rest`, `neptune_cluster_storage_encrypted`, `documentdb_cluster_storage_encrypted`, `opensearch_service_domains_encryption_at_rest_enabled`, `kinesis_stream_encrypted_at_rest`, `firehose_stream_encrypted_at_rest`, `sns_topics_kms_encryption_at_rest_enabled`, `sqs_queues_server_side_encryption_enabled`, `efs_encryption_at_rest_enabled`, `athena_workgroup_encryption`, `glue_data_catalogs_metadata_encryption_enabled`, `backup_vaults_encrypted`, `backup_recovery_point_encrypted`, `cloudtrail_kms_encryption_enabled`, `cloudwatch_log_group_kms_encryption_enabled`, `eks_cluster_kms_cmk_encryption_in_secrets_enabled`, `sagemaker_notebook_instance_encryption_enabled`, `apigateway_restapi_cache_encrypted`, `kafka_cluster_encryption_at_rest_uses_cmk`, `dynamodb_accelerator_cluster_encryption_enabled`, `storagegateway_fileshare_encryption_enabled` | `storage_infrastructure_encryption_is_enabled`, `storage_ensure_encryption_with_customer_managed_keys`, `vm_ensure_attached_disks_encrypted_with_cmk`, `vm_ensure_unattached_disks_encrypted_with_cmk`, `sqlserver_tde_encryption_enabled`, `sqlserver_tde_encrypted_with_cmk`, `databricks_workspace_cmk_encryption_enabled`, `monitor_storage_account_with_activity_logs_cmk_encrypted` | `compute_instance_encryption_with_csek_enabled`, `dataproc_encrypted_with_cmks_disabled`, `bigquery_dataset_cmk_encryption`, `bigquery_table_cmk_encryption` |
+| **CMEK required (customer-managed keys)** | `kms_cmk_are_used` | `storage_ensure_encryption_with_customer_managed_keys`, `vm_ensure_attached_disks_encrypted_with_cmk`, `vm_ensure_unattached_disks_encrypted_with_cmk`, `sqlserver_tde_encrypted_with_cmk`, `databricks_workspace_cmk_encryption_enabled` | `bigquery_dataset_cmk_encryption`, `bigquery_table_cmk_encryption`, `dataproc_encrypted_with_cmks_disabled`, `compute_instance_encryption_with_csek_enabled` |
+| **Key rotation enabled** | `kms_cmk_rotation_enabled` | `keyvault_key_rotation_enabled`, `storage_key_rotation_90_days` | `kms_key_rotation_enabled` |
+| **MFA for UI access** | `iam_root_mfa_enabled`, `iam_root_hardware_mfa_enabled`, `iam_user_mfa_enabled_console_access`, `iam_user_hardware_mfa_enabled`, `iam_administrator_access_with_mfa`, `cognito_user_pool_mfa_enabled` | `entra_privileged_user_has_mfa`, `entra_non_privileged_user_has_mfa`, `entra_user_with_vm_access_has_mfa`, `entra_security_defaults_enabled` | `compute_project_os_login_2fa_enabled` |
+| **API access / credentials** | `iam_no_root_access_key`, `iam_user_no_setup_initial_access_key`, `apigateway_restapi_authorizers_enabled`, `apigateway_restapi_public_with_authorizer`, `apigatewayv2_api_authorizers_enabled` | `entra_conditional_access_policy_require_mfa_for_management_api`, `app_function_access_keys_configured`, `app_function_identity_is_configured` | `apikeys_api_restrictions_configured`, `apikeys_key_exists`, `apikeys_key_rotated_in_90_days` |
+| **Log all admin/config changes** | `cloudtrail_multi_region_enabled`, `cloudtrail_multi_region_enabled_logging_management_events`, `cloudtrail_cloudwatch_logging_enabled`, `cloudtrail_log_file_validation_enabled`, `cloudwatch_log_metric_filter_*`, `cloudwatch_changes_to_*_alarm_configured`, `config_recorder_all_regions_enabled` | `monitor_diagnostic_settings_exists`, `monitor_diagnostic_setting_with_appropriate_categories`, `monitor_alert_*` | `iam_audit_logs_enabled`, `logging_log_metric_filter_and_alert_for_*`, `logging_sink_created` |
+| **Log integrity (digital signatures)** | `cloudtrail_log_file_validation_enabled` (exact) | None | None |
+| **Public access denied** | `s3_bucket_public_access`, `s3_bucket_public_list_acl`, `s3_bucket_public_write_acl`, `s3_account_level_public_access_blocks`, `apigateway_restapi_public`, `awslambda_function_url_public`, `awslambda_function_not_publicly_accessible`, `rds_instance_no_public_access`, `rds_snapshots_public_access`, `ec2_securitygroup_allow_ingress_from_internet_to_all_ports`, `sns_topics_not_publicly_accessible`, `sqs_queues_not_publicly_accessible` | `storage_blob_public_access_level_is_disabled`, `storage_ensure_private_endpoints_in_storage_accounts`, `containerregistry_not_publicly_accessible`, `keyvault_private_endpoints`, `app_function_not_publicly_accessible`, `aks_clusters_public_access_disabled`, `network_http_internet_access_restricted` | `cloudstorage_bucket_public_access`, `compute_instance_public_ip`, `cloudsql_instance_public_ip`, `compute_firewall_*_access_from_the_internet_allowed` |
+| **IAM least privilege** | `iam_*_no_administrative_privileges`, `iam_policy_allows_privilege_escalation`, `iam_inline_policy_allows_privilege_escalation`, `iam_role_administratoraccess_policy`, `iam_group_administrator_access_policy`, `iam_user_administrator_access_policy`, `iam_policy_attached_only_to_group_or_roles`, `iam_role_cross_service_confused_deputy_prevention` | `iam_role_user_access_admin_restricted`, `iam_subscription_roles_owner_custom_not_created`, `iam_custom_role_has_permissions_to_administer_resource_locks` | `iam_sa_no_administrative_privileges`, `iam_no_service_roles_at_project_level`, `iam_role_kms_enforce_separation_of_duties`, `iam_role_sa_enforce_separation_of_duties` |
+| **Password policy** | `iam_password_policy_minimum_length_14`, `iam_password_policy_uppercase`, `iam_password_policy_lowercase`, `iam_password_policy_symbol`, `iam_password_policy_number`, `iam_password_policy_expires_passwords_within_90_days_or_less`, `iam_password_policy_reuse_24` | None | None |
+| **Credential rotation / unused** | `iam_rotate_access_key_90_days`, `iam_user_accesskey_unused`, `iam_user_console_access_unused` | None | `iam_sa_user_managed_key_rotate_90_days`, `iam_sa_user_managed_key_unused`, `iam_service_account_unused` |
+| **VPC / flow logs** | `vpc_flow_logs_enabled` | `network_flow_log_captured_sent`, `network_watcher_enabled`, `network_flow_log_more_than_90_days` | `compute_subnet_flow_logs_enabled` |
+| **Backup / DR / Multi-AZ** | `backup_vaults_exist`, `backup_plans_exist`, `backup_reportplans_exist`, `rds_instance_backup_enabled`, `rds_*_protected_by_backup_plan`, `rds_cluster_multi_az`, `neptune_cluster_backup_enabled`, `documentdb_cluster_backup_enabled`, `efs_have_backup_enabled`, `s3_bucket_cross_region_replication`, `dynamodb_table_protected_by_backup_plan` | `vm_backup_enabled`, `vm_sufficient_daily_backup_retention_period`, `storage_geo_redundant_enabled` | `cloudsql_instance_automated_backups`, `cloudstorage_bucket_log_retention_policy_lock`, `cloudstorage_bucket_sufficient_retention_period` |
+| **Access analysis / discovery** | `accessanalyzer_enabled`, `accessanalyzer_enabled_without_findings` | None specific | `iam_account_access_approval_enabled`, `iam_cloud_asset_inventory_enabled` |
+| **Object lock / retention** | `s3_bucket_object_lock`, `s3_bucket_object_versioning`, `s3_bucket_lifecycle_enabled`, `cloudtrail_bucket_requires_mfa_delete`, `s3_bucket_no_mfa_delete` | `storage_ensure_soft_delete_is_enabled`, `storage_blob_versioning_is_enabled`, `storage_ensure_file_shares_soft_delete_is_enabled` | `cloudstorage_bucket_log_retention_policy_lock`, `cloudstorage_bucket_soft_delete_enabled`, `cloudstorage_bucket_versioning_enabled`, `cloudstorage_bucket_sufficient_retention_period` |
+| **Uniform bucket-level access** | `s3_bucket_acl_prohibited` | `storage_account_key_access_disabled`, `storage_default_to_entra_authorization_enabled` | `cloudstorage_bucket_uniform_bucket_level_access` |
+| **Container vulnerability scanning** | `ecr_registry_scan_images_on_push_enabled`, `ecr_repositories_scan_vulnerabilities_in_latest_image` | `defender_container_images_scan_enabled`, `defender_container_images_resolved_vulnerabilities` | `artifacts_container_analysis_enabled`, `gcr_container_scanning_enabled` |
+| **WAF / rate limiting** | `wafv2_webacl_with_rules`, `waf_*_webacl_with_rules`, `wafv2_webacl_logging_enabled`, `waf_global_webacl_logging_enabled` | None | None |
+| **Deployment region restriction** | `organizations_scp_check_deny_regions` | None | None |
+| **Secrets automatic rotation** | `secretsmanager_automatic_rotation_enabled`, `secretsmanager_secret_rotated_periodically` | `keyvault_rbac_secret_expiration_set`, `keyvault_non_rbac_secret_expiration_set` | None |
+| **Certificate management** | `acm_certificates_expiration_check`, `acm_certificates_with_secure_key_algorithms`, `acm_certificates_transparency_logs_enabled` | `keyvault_key_expiration_set_in_non_rbac`, `keyvault_rbac_key_expiration_set`, `keyvault_non_rbac_secret_expiration_set` | None |
+| **GenAI guardrails / input/output filtering** | `bedrock_guardrail_prompt_attack_filter_enabled`, `bedrock_guardrail_sensitive_information_filter_enabled`, `bedrock_agent_guardrail_enabled`, `bedrock_model_invocation_logging_enabled`, `bedrock_api_key_no_administrative_privileges`, `bedrock_api_key_no_long_term_credentials` | None | None |
+| **ML dev environment security** | `sagemaker_notebook_instance_root_access_disabled`, `sagemaker_notebook_instance_without_direct_internet_access_configured`, `sagemaker_notebook_instance_vpc_settings_configured`, `sagemaker_models_vpc_settings_configured`, `sagemaker_training_jobs_vpc_settings_configured`, `sagemaker_training_jobs_network_isolation_enabled`, `sagemaker_training_jobs_volume_and_output_encryption_enabled` | None | None |
+| **Threat detection / anomalous behavior** | `cloudtrail_threat_detection_enumeration`, `cloudtrail_threat_detection_privilege_escalation`, `cloudtrail_threat_detection_llm_jacking`, `guardduty_is_enabled`, `guardduty_no_high_severity_findings` | None | None |
+| **Serverless private access** | `awslambda_function_inside_vpc`, `awslambda_function_not_publicly_accessible`, `awslambda_function_url_public` | `app_function_not_publicly_accessible` | None |
+
+### What Prowler Does NOT Cover (accept MANUAL honestly)
+
+Don't pad mappings for these — mark `Checks: []` and move on:
+
+- **TLS 1.3 version specifically** — Prowler verifies TLS is enforced, not always the exact version
+- **IANA port-protocol consistency** — no check for "protocol running on its assigned port"
+- **mTLS on most Azure/GCP services** — limited to App Service client certs on Azure, nothing on GCP
+- **Rate limiting** on monitoring endpoints, load balancers, serverless invocations, vector ingestion
+- **Session cookie expiry** (LB stickiness)
+- **HTTP header scrubbing** (Server, X-Powered-By)
+- **Certificate transparency verification for imports**
+- **Model version pinning, red teaming, AI quality review**
+- **Vector embedding validation, dimensional constraints, ANN vs exact search**
+- **Secret region replication** (cross-region residency)
+- **Lifecycle cleanup policies on container registries**
+- **Row-level / column-level security in data warehouses**
+- **Deployment region restriction on Azure/GCP** (AWS has `organizations_scp_check_deny_regions`, others don't)
+- **Cross-tenant alert silencing permissions**
+- **Field-level masking in logs**
+- **Managed view enforcement for database access**
+- **Automatic MFA delete on all S3 buckets** (only CloudTrail bucket variant exists for some frameworks — AWS has the generic `s3_bucket_no_mfa_delete` though)
+
+---
+
+## Workflow C: Add a New Output Formatter
+
+Use when a new framework needs its own CSV columns or terminal table. Follow the c5/csa/ens layout exactly:
+
+```bash
+mkdir -p prowler/lib/outputs/compliance/{framework}
+touch prowler/lib/outputs/compliance/{framework}/__init__.py
+```
+
+### Step 1 — Create `{framework}.py` (table dispatcher ONLY)
+
+Copy from `prowler/lib/outputs/compliance/c5/c5.py` and change the function name + framework string. The `diff` between your file and `c5.py` should be just those two lines. **No function docstring** — other frameworks don't have one, stay consistent.
+
+### Step 2 — Create `models.py`
+
+One Pydantic v2 `BaseModel` per provider. Field names become CSV column headers (public API — don't rename later without a migration).
+
+```python
+from typing import Optional
+from pydantic import BaseModel
+
+class {Framework}_AWSModel(BaseModel):
+ Provider: str
+ Description: str
+ AccountId: str
+ Region: str
+ AssessmentDate: str
+ Requirements_Id: str
+ Requirements_Description: str
+ # ... provider-specific columns
+ Status: str
+ StatusExtended: str
+ ResourceId: str
+ ResourceName: str
+ CheckId: str
+ Muted: bool
+```
+
+### Step 3 — Create `{framework}_{provider}.py` for each provider
+
+Copy from `prowler/lib/outputs/compliance/c5/c5_aws.py` etc. Contains the `{Framework}_AWS(ComplianceOutput)` class with `transform()` that walks findings and emits model rows. This file IS allowed to import `Finding`.
+
+### Step 4 — Register everywhere
+
+**`prowler/lib/outputs/compliance/compliance.py`** (CLI table dispatcher):
+```python
+from prowler.lib.outputs.compliance.{framework}.{framework} import get_{framework}_table
+
+def display_compliance_table(...):
+ ...
+ elif compliance_framework.startswith("{framework}_"):
+ get_{framework}_table(findings, bulk_checks_metadata,
+ compliance_framework, output_filename,
+ output_directory, compliance_overview)
+```
+
+**`prowler/__main__.py`** (CLI output writer per provider):
+Add imports at the top:
+```python
+from prowler.lib.outputs.compliance.{framework}.{framework}_aws import {Framework}_AWS
+from prowler.lib.outputs.compliance.{framework}.{framework}_azure import {Framework}_Azure
+from prowler.lib.outputs.compliance.{framework}.{framework}_gcp import {Framework}_GCP
+```
+Add provider-specific `elif compliance_name.startswith("{framework}_"):` branches that instantiate the class and call `batch_write_data_to_file()`.
+
+**`api/src/backend/tasks/jobs/export.py`** (API export dispatcher):
+```python
+from prowler.lib.outputs.compliance.{framework}.{framework}_aws import {Framework}_AWS
+# ... azure, gcp
+
+COMPLIANCE_CLASS_MAP = {
+ "aws": [
+ # ...
+ (lambda name: name.startswith("{framework}_"), {Framework}_AWS),
+ ],
+ # ... azure, gcp
+}
+```
+
+**Always use `startswith`**, never `name == "framework_aws"`. Exact match is a regression.
+
+### Step 5 — Add tests
+
+Create `tests/lib/outputs/compliance/{framework}/` with `{framework}_aws_test.py`, `{framework}_azure_test.py`, `{framework}_gcp_test.py`. See the test template in [references/test_template.md](references/test_template.md).
+
+Add fixtures to `tests/lib/outputs/compliance/fixtures.py`: one `Compliance` object per provider with 1 evaluated + 1 manual requirement to exercise both code paths in `transform()`.
+
+### Circular import warning
+
+**The table dispatcher file (`{framework}.py`) MUST NOT import `Finding`** (directly or transitively). The cycle is:
+
+```
+compliance.compliance imports get_{framework}_table
+ → {framework}.py imports ComplianceOutput
+ → compliance_output imports Finding
+ → finding imports get_check_compliance from compliance.compliance
+ → CIRCULAR
+```
+
+Keep `{framework}.py` bare — only `colorama`, `tabulate`, `prowler.config.config`. Put anything that imports `Finding` in the per-provider `{framework}_{provider}.py` files.
+
+---
+
+## Conventions and Hard-Won Gotchas
+
+These are lessons from the FINOS CCC v2025.10 sync + 172-AR audit pass (April 2026). Learn them once; save days of debugging.
+
+1. **Per-provider files are non-negotiable.** Never collapse `{framework}_aws.py`, `{framework}_azure.py`, `{framework}_gcp.py` into a single parameterized class, no matter how DRY-tempting. Every other framework in the codebase follows the per-provider pattern and reviewers will reject the refactor. The CSV column names differ per provider — three classes is the convention.
+2. **`{framework}.py` has NO function docstring.** Other frameworks don't have them. Don't add one to be "helpful".
+3. **Circular import protection**: the table dispatcher file MUST NOT import `Finding` (directly or transitively). Split the code so `{framework}.py` only has `get_{framework}_table()` with bare imports, and `{framework}_{provider}.py` holds the class that needs `Finding`.
+4. **`Generic_Compliance_Requirement_Attribute` is the fallback** — in the `Compliance_Requirement.Attributes` Union in `compliance_models.py`, Generic MUST be LAST because Pydantic v1 tries union members in order. Putting Generic first means every framework-specific attribute falls through to Generic and the specific model is never used.
+5. **Pydantic v1 imports.** `from pydantic.v1 import BaseModel` in `compliance_models.py` — not v2. Mixing causes validation errors. Pydantic v2 is used in the CSV models (`models.py`) — that's fine because they're separate trees.
+6. **`get_check_compliance()` key format** is `f"{Framework}-{Version}"` ONLY if Version is set. Empty Version → key is `"{Framework}"` (no version suffix). Tests that mock compliance dicts must match this exact format — when a framework ships with `Version: ""`, downstream code and tests break silently.
+7. **CSV column names from `models.py` are public API.** Don't rename a field without migrating downstream consumers — CSV headers change.
+8. **Upstream YAML multi-line scalars** (`|` block scalars) preserve newlines. Collapse to single-line with `" ".join(value.split())` before writing to JSON.
+9. **Upstream catalogs can use multiple shapes.** FINOS CCC uses `control-families: [...]` in most catalogs but `controls: [...]` at the top level in `storage/object`. Any sync script must handle both or silently drop entire catalogs.
+10. **Foreign-prefix AR ids.** Upstream sometimes "imports" requirements from one catalog into another by keeping the original id prefix (e.g., `CCC.AuditLog.CN08.AR01` appearing under `CCC.Logging.CN03`). Prowler's compliance model requires unique ids within a catalog — rewrite the foreign id to fit the parent control: `CCC.AuditLog.CN08.AR01` (inside `CCC.Logging.CN03`) → `CCC.Logging.CN03.AR01`.
+11. **Genuine upstream id collisions.** Sometimes upstream has a real typo where two different requirements share the same id (e.g., `CCC.Core.CN14.AR02` defined twice for 30-day and 14-day backup variants). Renumber the second copy to the next free AR number. Preserve check mappings by matching on `(Section, frozenset(Applicability))` since the renumbered id won't match by id.
+12. **`COMPLIANCE_CLASS_MAP` in `export.py` uses `startswith` predicates** for all modern frameworks. Exact match (`name == "ccc_aws"`) is an anti-pattern — it was present for CCC until April 2026 and was the reason CCC couldn't have versioned variants.
+13. **Pre-validate every check id** against the per-provider inventory before writing the JSON. A typo silently creates an unreferenced check that will fail when findings try to map to it. The audit script MUST abort with stderr listing typos, not swallow them.
+14. **REPLACE is better than PATCH** for audit decisions. Encoding every mapping explicitly makes the audit reproducible and surfaces hidden assumptions from the legacy data. A PATCH system that adds/removes is too easy to forget.
+15. **When no check applies, MANUAL is correct.** Do not pad mappings with tangential checks "just in case". Prowler's compliance reports are meant to be actionable — padding them with noise breaks that. Honest manual reqs can be mapped later when new checks land.
+16. **UI groups by `Attributes[0].FamilyName` and `Attributes[0].Section`.** If FamilyName has inconsistent variants within the same JSON (e.g., "Logging & Monitoring" vs "Logging and Monitoring"), the UI renders them as separate categories. Section empty → the requirement falls into an orphan control with label "". Normalize before shipping.
+17. **Provider coverage is asymmetric.** AWS has dense coverage (~586 checks across 80+ services): in-transit encryption, IAM, database encryption, backup. Azure (~167 checks) and GCP (~102 checks) are thinner especially for in-transit encryption, mTLS, and ML/AI. Accept the asymmetry in mappings — don't force GCP parity where Prowler genuinely can't verify.
+
+---
+
+## Useful One-Liners
+
+```bash
+# Count requirements per service prefix (CCC, CIS sections, etc.)
+jq -r '.Requirements[].Id | split(".")[1]' prowler/compliance/aws/ccc_aws.json | sort | uniq -c
+
+# Find duplicate requirement IDs
+jq -r '.Requirements[].Id' file.json | sort | uniq -d
+
+# Count manual requirements (no checks)
+jq '[.Requirements[] | select((.Checks | length) == 0)] | length' file.json
+
+# List all unique check references in a framework
+jq -r '.Requirements[].Checks[]' file.json | sort -u
+
+# List all unique Sections (to spot inconsistency)
+jq '[.Requirements[].Attributes[0].Section] | unique' file.json
+
+# List all unique FamilyNames (to spot inconsistency)
+jq '[.Requirements[].Attributes[0].FamilyName] | unique' file.json
+
+# Diff requirement ids between two versions of the same framework
+diff <(jq -r '.Requirements[].Id' a.json | sort) <(jq -r '.Requirements[].Id' b.json | sort)
+
+# Find where a check id is used across all frameworks
+grep -rl "my_check_name" prowler/compliance/
+
+# Check if a Prowler check exists
+find prowler/providers/aws/services -name "{check_id}.metadata.json"
+
+# Validate a JSON with Pydantic
+python -c "from prowler.lib.check.compliance_models import Compliance; print(Compliance.parse_file('prowler/compliance/aws/ccc_aws.json').Framework)"
+```
+
+---
+
## Best Practices
1. **Requirement IDs**: Follow the original framework numbering exactly (e.g., "1.1", "A.5.1", "T1190", "ac_2_1")
-2. **Check Mapping**: Map to existing checks when possible. Use `Checks: []` for manual-only requirements
+2. **Check Mapping**: Map to existing checks when possible. Use `Checks: []` for manual-only requirements — honest MANUAL beats padded coverage
3. **Completeness**: Include all framework requirements, even those without automated checks
-4. **Version Control**: Include framework version in `Name` and `Version` fields
+4. **Version Control**: Include framework version in `Name` and `Version` fields. **Never leave `Version: ""`** — it breaks `get_check_compliance()` key format
5. **File Naming**: Use format `{framework}_{version}_{provider}.json`
-6. **Validation**: Prowler validates JSON against Pydantic models at startup - invalid JSON will cause errors
+6. **Validation**: Prowler validates JSON against Pydantic models at startup — invalid JSON will cause errors
+7. **Pre-validate check ids** against the provider's `*.metadata.json` inventory before every commit
+8. **Normalize FamilyName and Section** to avoid inconsistent UI tree branches
+9. **Register everywhere**: SDK model (if needed) → `compliance.py` dispatcher → `__main__.py` CLI writer → `export.py` API map → UI mapper. Skipping any layer results in silent failures
+10. **Audit, don't pad**: when reviewing mappings, apply the golden rule — the check's title/risk MUST literally describe what the requirement text says. Tangential relation doesn't count
## Commands
@@ -482,11 +1005,46 @@ prowler aws --compliance cis_5.0_aws -M csv json html
## Code References
-- **Compliance Models:** `prowler/lib/check/compliance_models.py`
-- **Compliance Processing:** `prowler/lib/check/compliance.py`
-- **Compliance Output:** `prowler/lib/outputs/compliance/`
+### Layer 1 — SDK / Core
+- **Compliance Models:** `prowler/lib/check/compliance_models.py` (Pydantic v1 model tree)
+- **Compliance Processing / Linker:** `prowler/lib/check/compliance.py` (`get_check_compliance`, `update_checks_metadata_with_compliance`)
+- **Check Utils:** `prowler/lib/check/utils.py` (`list_compliance_modules`)
+
+### Layer 2 — JSON Catalogs
+- **Framework JSONs:** `prowler/compliance/{provider}/` (auto-discovered via directory walk)
+
+### Layer 3 — Output Formatters
+- **Per-framework folders:** `prowler/lib/outputs/compliance/{framework}/`
+- **Shared base class:** `prowler/lib/outputs/compliance/compliance_output.py` (`ComplianceOutput` + `batch_write_data_to_file`)
+- **CLI table dispatcher:** `prowler/lib/outputs/compliance/compliance.py` (`display_compliance_table`)
+- **Finding model:** `prowler/lib/outputs/finding.py` (**do not import transitively from table dispatcher files — circular import**)
+- **CLI writer:** `prowler/__main__.py` (per-provider `elif compliance_name.startswith(...)` branches that instantiate per-provider classes)
+
+### Layer 4 — API / UI
+- **API lazy loader:** `api/src/backend/api/compliance.py` (`LazyComplianceTemplate`, `LazyChecksMapping`)
+- **API export dispatcher:** `api/src/backend/tasks/jobs/export.py` (`COMPLIANCE_CLASS_MAP` with `startswith` predicates)
+- **UI framework router:** `ui/lib/compliance/compliance-mapper.ts`
+- **UI per-framework mapper:** `ui/lib/compliance/{framework}.tsx`
+- **UI detail panel:** `ui/components/compliance/compliance-custom-details/{framework}-details.tsx`
+- **UI types:** `ui/types/compliance.ts`
+- **UI icon:** `ui/components/icons/compliance/{framework}.svg` + registration in `IconCompliance.tsx`
+
+### Tests
+- **Output formatter tests:** `tests/lib/outputs/compliance/{framework}/{framework}_{provider}_test.py`
+- **Shared fixtures:** `tests/lib/outputs/compliance/fixtures.py`
## Resources
-- **Templates:** See [assets/](assets/) for framework JSON templates
+- **JSON Templates:** See [assets/](assets/) for framework JSON templates (cis, ens, iso27001, mitre_attack, prowler_threatscore, generic)
+- **Config-driven compliance sync** (any upstream-backed framework):
+ - [assets/sync_framework.py](assets/sync_framework.py) — generic runner. Loads a YAML config, dynamically imports the declared parser, applies generic post-processing (id uniqueness safety net, `FamilyName` normalization, legacy check-mapping preservation with config-driven fallback keys), and writes the provider JSONs with Pydantic post-validation. Framework-agnostic — works for any compliance framework.
+ - [assets/configs/ccc.yaml](assets/configs/ccc.yaml) — canonical config example (FINOS CCC v2025.10). Copy and adapt for new frameworks.
+ - [assets/parsers/finos_ccc.py](assets/parsers/finos_ccc.py) — FINOS CCC YAML parser. Handles both upstream shapes (`control-families` and top-level `controls`), foreign-prefix AR rewriting, and genuine collision renumbering. Exposes `parse_upstream(config) -> list[dict]`.
+ - [assets/parsers/](assets/parsers/) — add new parser modules here for unfamiliar upstream formats (NIST OSCAL JSON, MITRE STIX, CIS Benchmarks, etc.). Each parser is a `{name}.py` file implementing `parse_upstream(config) -> list[dict]` with guaranteed-unique ids.
+- **Reusable audit tooling** (added April 2026 after the FINOS CCC v2025.10 sync):
+ - [assets/audit_framework_template.py](assets/audit_framework_template.py) — explicit REPLACE decision ledger with pre-validation against the per-provider inventory. Drop-in template for auditing any framework.
+ - [assets/query_checks.py](assets/query_checks.py) — keyword/service/id query helper over `/tmp/checks_{provider}.json`.
+ - [assets/dump_section.py](assets/dump_section.py) — dumps every AR for a given id prefix across all 3 providers with current check mappings.
+ - [assets/build_inventory.py](assets/build_inventory.py) — generates `/tmp/checks_{provider}.json` from `*.metadata.json` files.
- **Documentation:** See [references/compliance-docs.md](references/compliance-docs.md) for additional resources
+- **Related skill:** [prowler-compliance-review](../prowler-compliance-review/SKILL.md) — PR review checklist and validator script for compliance framework PRs
diff --git a/skills/prowler-compliance/assets/audit_framework_template.py b/skills/prowler-compliance/assets/audit_framework_template.py
new file mode 100644
index 0000000000..f2d58603d7
--- /dev/null
+++ b/skills/prowler-compliance/assets/audit_framework_template.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python3
+"""
+Cloud-auditor pass template for any Prowler compliance framework.
+
+Encode explicit REPLACE decisions per (requirement_id, provider) pair below.
+Each decision FULLY overwrites the legacy Checks list for that requirement.
+
+Workflow:
+ 1. Run build_inventory.py first to cache per-provider check metadata.
+ 2. Run dump_section.py to see current mappings for the catalog you're auditing.
+ 3. Fill in DECISIONS below with explicit check lists.
+ 4. Run this script — it pre-validates every check id against the inventory
+ and aborts with stderr listing typos before writing.
+
+Decision rules (apply as a hostile cloud auditor):
+ - The Prowler check's title/risk MUST literally describe what the AR text says.
+ "Related" is not enough.
+ - If no check actually addresses the requirement, leave `[]` (= MANUAL).
+ HONEST MANUAL is worth more than padded coverage.
+ - Missing provider key = leave the legacy mapping untouched.
+ - Empty list `[]` = explicitly MANUAL (overwrites legacy).
+
+Usage:
+ # 1. Copy this file to /tmp/audit_.py and fill in DECISIONS
+ # 2. Edit FRAMEWORK_KEY below to match your framework file naming
+ # 3. Run:
+ python /tmp/audit_.py
+"""
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+# ---------------------------------------------------------------------------
+# Configure for your framework
+# ---------------------------------------------------------------------------
+
+# Framework file basename inside prowler/compliance/{provider}/.
+# If your framework is called "cis_5.0_aws.json", FRAMEWORK_KEY is "cis_5.0".
+# If the file is "ccc_aws.json", FRAMEWORK_KEY is "ccc".
+FRAMEWORK_KEY = "ccc"
+
+# Which providers to apply decisions to.
+PROVIDERS = ["aws", "azure", "gcp"]
+
+PROWLER_DIR = Path("prowler/compliance")
+CHECK_INV = {prov: Path(f"/tmp/checks_{prov}.json") for prov in PROVIDERS}
+
+
+# ---------------------------------------------------------------------------
+# DECISIONS — encode one entry per requirement you want to audit
+# ---------------------------------------------------------------------------
+
+# DECISIONS[requirement_id][provider] = list[str] of check ids
+# See SKILL.md → "Audit Reference Table: Requirement Text → Prowler Checks"
+# for a comprehensive mapping cheat sheet built from a 172-AR CCC audit.
+
+DECISIONS: dict[str, dict[str, list[str]]] = {}
+
+# ---- Example entries (delete and replace with your own) ----
+
+# Example 1: TLS in transit enforced (non-SSH traffic)
+# DECISIONS["CCC.Core.CN01.AR01"] = {
+# "aws": [
+# "cloudfront_distributions_https_enabled",
+# "cloudfront_distributions_origin_traffic_encrypted",
+# "s3_bucket_secure_transport_policy",
+# "elbv2_ssl_listeners",
+# "rds_instance_transport_encrypted",
+# "kafka_cluster_in_transit_encryption_enabled",
+# "redshift_cluster_in_transit_encryption_enabled",
+# "opensearch_service_domains_https_communications_enforced",
+# ],
+# "azure": [
+# "storage_secure_transfer_required_is_enabled",
+# "app_minimum_tls_version_12",
+# "postgresql_flexible_server_enforce_ssl_enabled",
+# "sqlserver_recommended_minimal_tls_version",
+# ],
+# "gcp": [
+# "cloudsql_instance_ssl_connections",
+# ],
+# }
+
+# Example 2: MANUAL — no Prowler check exists
+# DECISIONS["CCC.Core.CN01.AR07"] = {
+# "aws": [], # no IANA port/protocol check exists in Prowler
+# "azure": [],
+# "gcp": [],
+# }
+
+# Example 3: Reuse a decision for multiple sibling ARs
+# DECISIONS["CCC.ObjStor.CN05.AR02"] = DECISIONS["CCC.ObjStor.CN05.AR01"]
+
+
+# ---------------------------------------------------------------------------
+# Driver — do not edit below
+# ---------------------------------------------------------------------------
+
+def load_inventory(provider: str) -> dict:
+ path = CHECK_INV[provider]
+ if not path.exists():
+ raise SystemExit(
+ f"Check inventory missing: {path}\n"
+ f"Run: python skills/prowler-compliance/assets/build_inventory.py {provider}"
+ )
+ with open(path) as f:
+ return json.load(f)
+
+
+def resolve_json_path(provider: str) -> Path:
+ """Resolve the JSON file path for a given provider.
+
+ Handles both shapes: {FRAMEWORK_KEY}_{provider}.json (ccc_aws.json) and
+ cases where FRAMEWORK_KEY already contains the provider suffix.
+ """
+ candidates = [
+ PROWLER_DIR / provider / f"{FRAMEWORK_KEY}_{provider}.json",
+ PROWLER_DIR / provider / f"{FRAMEWORK_KEY}.json",
+ ]
+ for c in candidates:
+ if c.exists():
+ return c
+ raise SystemExit(
+ f"Could not find framework JSON for provider={provider} "
+ f"with FRAMEWORK_KEY={FRAMEWORK_KEY}. Tried: {candidates}"
+ )
+
+
+def plan_for_provider(
+ provider: str,
+) -> tuple[Path, dict, tuple[int, int, int], list[tuple[str, str]]]:
+ """Build the updated JSON for one provider without writing it.
+
+ Returns (path, mutated_data, (touched, added, removed), unknowns).
+ Writing is deferred to a second pass so that a typo in any provider
+ aborts the whole run before any file on disk changes.
+ """
+ path = resolve_json_path(provider)
+ with open(path) as f:
+ data = json.load(f)
+ inv = load_inventory(provider)
+
+ touched = 0
+ add_count = 0
+ rm_count = 0
+ unknown: list[tuple[str, str]] = []
+
+ for req in data["Requirements"]:
+ rid = req["Id"]
+ if rid not in DECISIONS or provider not in DECISIONS[rid]:
+ continue
+ new_checks = list(dict.fromkeys(DECISIONS[rid][provider]))
+ for c in new_checks:
+ if c not in inv:
+ unknown.append((rid, c))
+ before = set(req.get("Checks") or [])
+ after = set(new_checks)
+ rm_count += len(before - after)
+ add_count += len(after - before)
+ req["Checks"] = new_checks
+ touched += 1
+
+ return path, data, (touched, add_count, rm_count), unknown
+
+
+def main() -> int:
+ if not DECISIONS:
+ print("No DECISIONS encoded. Fill in the DECISIONS dict and re-run.")
+ return 1
+ print(f"Applying {len(DECISIONS)} decisions to framework '{FRAMEWORK_KEY}'...")
+
+ # Pass 1: validate every provider before touching disk. A typo in any
+ # provider must abort the run before ANY file has been rewritten.
+ plans: list[tuple[str, Path, dict, tuple[int, int, int]]] = []
+ all_unknown: list[tuple[str, str, str]] = []
+ for provider in PROVIDERS:
+ path, data, counts, unknown = plan_for_provider(provider)
+ for rid, c in unknown:
+ all_unknown.append((provider, rid, c))
+ plans.append((provider, path, data, counts))
+
+ if all_unknown:
+ print("\n!! UNKNOWN CHECK IDS (typos?):", file=sys.stderr)
+ for provider, rid, c in all_unknown:
+ print(f" {provider} {rid} -> {c}", file=sys.stderr)
+ print(
+ "\nAborting: fix the check ids above and re-run. "
+ "No files were modified.",
+ file=sys.stderr,
+ )
+ return 2
+
+ # Pass 2: all providers validated cleanly — write.
+ for provider, path, data, (touched, added, removed) in plans:
+ with open(path, "w") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+ f.write("\n")
+ print(
+ f" {provider}: touched={touched} added={added} removed={removed}"
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/prowler-compliance/assets/build_inventory.py b/skills/prowler-compliance/assets/build_inventory.py
new file mode 100644
index 0000000000..f743aa75a5
--- /dev/null
+++ b/skills/prowler-compliance/assets/build_inventory.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+"""
+Build a per-provider check inventory by scanning Prowler's check metadata files.
+
+Outputs one JSON per provider at /tmp/checks_{provider}.json with the shape:
+ {
+ "check_id": {
+ "service": "...",
+ "subservice": "...",
+ "resource": "...",
+ "severity": "...",
+ "title": "...",
+ "description": "...",
+ "risk": "..."
+ },
+ ...
+ }
+
+This is the reference used by audit_framework_template.py for pre-validation
+(every check id in the audit ledger must exist in the inventory) and by
+query_checks.py for keyword/service lookup.
+
+Usage:
+ python skills/prowler-compliance/assets/build_inventory.py
+ # Or for a specific provider:
+ python skills/prowler-compliance/assets/build_inventory.py aws
+
+Output:
+ /tmp/checks_{provider}.json for every provider discovered under
+ prowler/providers/ with a services/ directory.
+"""
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+PROVIDERS_ROOT = Path("prowler/providers")
+
+
+def discover_providers() -> list[str]:
+ """Return every provider that currently has a services/ directory.
+
+ Derived from the filesystem so new providers are picked up automatically
+ and stale hard-coded lists cannot drift from the repo.
+ """
+ if not PROVIDERS_ROOT.exists():
+ return []
+ return sorted(
+ p.name
+ for p in PROVIDERS_ROOT.iterdir()
+ if p.is_dir() and (p / "services").is_dir()
+ )
+
+
+def build_for_provider(provider: str) -> dict:
+ inventory: dict[str, dict] = {}
+ base = Path(f"prowler/providers/{provider}/services")
+ if not base.exists():
+ print(f" skip {provider}: no services directory", file=sys.stderr)
+ return inventory
+ for meta_path in base.rglob("*.metadata.json"):
+ try:
+ with open(meta_path) as f:
+ data = json.load(f)
+ except Exception as exc:
+ print(f" warn: cannot parse {meta_path}: {exc}", file=sys.stderr)
+ continue
+ cid = data.get("CheckID") or meta_path.stem.replace(".metadata", "")
+ inventory[cid] = {
+ "service": data.get("ServiceName", ""),
+ "subservice": data.get("SubServiceName", ""),
+ "resource": data.get("ResourceType", ""),
+ "severity": data.get("Severity", ""),
+ "title": data.get("CheckTitle", ""),
+ "description": data.get("Description", ""),
+ "risk": data.get("Risk", ""),
+ }
+ return inventory
+
+
+def main() -> int:
+ providers = sys.argv[1:] or discover_providers()
+ if not providers:
+ print(
+ f"error: no providers found under {PROVIDERS_ROOT}/",
+ file=sys.stderr,
+ )
+ return 1
+ for provider in providers:
+ inv = build_for_provider(provider)
+ out_path = Path(f"/tmp/checks_{provider}.json")
+ with open(out_path, "w") as f:
+ json.dump(inv, f, indent=2)
+ print(f" {provider}: {len(inv)} checks → {out_path}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/prowler-compliance/assets/configs/ccc.yaml b/skills/prowler-compliance/assets/configs/ccc.yaml
new file mode 100644
index 0000000000..deb757ffc9
--- /dev/null
+++ b/skills/prowler-compliance/assets/configs/ccc.yaml
@@ -0,0 +1,120 @@
+# FINOS Common Cloud Controls (CCC) sync config for sync_framework.py.
+#
+# Usage:
+# python skills/prowler-compliance/assets/sync_framework.py \
+# skills/prowler-compliance/assets/configs/ccc.yaml
+#
+# Prerequisite: run the upstream fetch step from SKILL.md Workflow A Step 1 to
+# populate upstream.dir with the raw FINOS catalog YAML files.
+
+framework:
+ name: CCC
+ display_name: Common Cloud Controls Catalog (CCC)
+ version: v2025.10
+ # The {provider_display} placeholder is replaced at output time with the
+ # per-provider display string from the providers list below.
+ description_template: "Common Cloud Controls Catalog (CCC) for {provider_display}"
+
+providers:
+ - key: aws
+ display: AWS
+ - key: azure
+ display: Azure
+ - key: gcp
+ display: GCP
+
+output:
+ # Supported placeholders: {provider}, {framework}, {version}.
+ # For versioned frameworks like CIS the template would be
+ # "prowler/compliance/{provider}/cis_{version}_{provider}.json".
+ path_template: "prowler/compliance/{provider}/ccc_{provider}.json"
+
+upstream:
+ # Directory containing the cached FINOS catalog YAMLs. Populate via
+ # SKILL.md Workflow A Step 1 (gh api raw download commands).
+ dir: /tmp/ccc_upstream
+ fetch_docs: "See SKILL.md Workflow A Step 1 for gh api fetch commands"
+
+parser:
+ # Name of the parser module under parsers/ (loaded dynamically by the
+ # runner). For FINOS CCC YAML this is always finos_ccc.
+ module: finos_ccc
+
+ # FINOS CCC catalog files in load order. Core first so its ARs render
+ # first in the output JSON.
+ catalog_files:
+ - core_ccc.yaml
+ - management_auditlog.yaml
+ - management_logging.yaml
+ - management_monitoring.yaml
+ - storage_object.yaml
+ - networking_loadbalancer.yaml
+ - networking_vpc.yaml
+ - crypto_key.yaml
+ - crypto_secrets.yaml
+ - database_warehouse.yaml
+ - database_vector.yaml
+ - database_relational.yaml
+ - devtools_build.yaml
+ - devtools_container-registry.yaml
+ - identity_iam.yaml
+ - ai-ml_gen-ai.yaml
+ - ai-ml_mlde.yaml
+ - app-integration_message.yaml
+ - compute_serverless-computing.yaml
+
+ # Shape-2 catalogs (storage/object) reference the family via id only
+ # (e.g. "CCC.ObjStor.Data") with no human-readable title or description
+ # in the YAML. Map the suffix (after the last dot) to a canonical title
+ # and description so the generated JSON has consistent FamilyName fields
+ # regardless of upstream shape.
+ family_id_title:
+ Data: Data
+ IAM: Identity and Access Management
+ Identity: Identity and Access Management
+ Encryption: Encryption
+ Logging: Logging and Monitoring
+ Network: Network Security
+ Availability: Availability
+ Integrity: Integrity
+ Confidentiality: Confidentiality
+ family_id_description:
+ Data: "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle."
+ IAM: "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources."
+
+post_processing:
+ # Collapse FamilyName variants that appear inconsistently across upstream
+ # catalogs. The Prowler UI groups by Attributes[0].FamilyName exactly,
+ # so each variant would otherwise become a separate tree branch.
+ family_name_normalization:
+ "Logging & Monitoring": "Logging and Monitoring"
+ "Logging and Metrics Publication": "Logging and Monitoring"
+
+ # Preserve existing Checks lists from the legacy Prowler JSON when
+ # regenerating. The runner builds two lookup tables from the legacy
+ # output: a primary index by Id, and fallback indexes composed of
+ # attribute field names.
+ #
+ # primary_key: the top-level requirement field to use as the primary
+ # lookup key (almost always "Id")
+ # fallback_keys: a list of composite keys. Each composite key is a list
+ # of Attributes[0] field names to join into a tuple. List-valued fields
+ # (like Applicability) are frozen to frozenset so the tuple is hashable.
+ #
+ # CCC uses (Section, Applicability) because Applicability is a CCC-only
+ # top-level attribute field. CIS would use (Section, Profile). NIST would
+ # use (ItemId,). The fallback is how renumbered or rewritten ids still
+ # recover their check mappings.
+ #
+ # legacy_path_template (optional): path to read legacy Checks FROM.
+ # Defaults to output.path_template, which is correct for unversioned
+ # frameworks (like CCC) where regeneration overwrites the same file.
+ # For versioned frameworks that write to a new file on each version
+ # bump (e.g. cis_5.1_aws.json while the legacy mappings live in
+ # cis_5.0_aws.json), set this to the previous-version path so Checks
+ # are preserved instead of lost:
+ # legacy_path_template: "prowler/compliance/{provider}/cis_5.0_{provider}.json"
+ check_preservation:
+ primary_key: Id
+ fallback_keys:
+ - [Section, Applicability]
diff --git a/skills/prowler-compliance/assets/dump_section.py b/skills/prowler-compliance/assets/dump_section.py
new file mode 100644
index 0000000000..ca2fff0e1b
--- /dev/null
+++ b/skills/prowler-compliance/assets/dump_section.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+"""
+Dump every requirement of a compliance framework for a given id prefix across
+providers, with their current Check mappings.
+
+Useful for reviewing a whole control family in one pass before encoding audit
+decisions in audit_framework_template.py.
+
+Usage:
+ # Dump all CCC.Core requirements across aws/azure/gcp
+ python skills/prowler-compliance/assets/dump_section.py ccc "CCC.Core."
+
+ # Dump all CIS 5.0 section 1 requirements for AWS only
+ python skills/prowler-compliance/assets/dump_section.py cis_5.0_aws "1."
+
+Arguments:
+ framework_key: file prefix inside prowler/compliance/{provider}/ without
+ the provider suffix. Examples:
+ - "ccc" → loads ccc_aws.json / ccc_azure.json / ccc_gcp.json
+ - "cis_5.0_aws" → loads only that one file
+ - "iso27001_2022" → loads all providers
+ id_prefix: Requirement id prefix to filter by (e.g. "CCC.Core.",
+ "1.1.", "A.5.").
+"""
+from __future__ import annotations
+
+import json
+import sys
+from collections import defaultdict
+from pathlib import Path
+
+PROWLER_COMPLIANCE_DIR = Path("prowler/compliance")
+
+
+def main() -> int:
+ if len(sys.argv) < 3:
+ print(__doc__)
+ return 1
+
+ framework_key = sys.argv[1]
+ id_prefix = sys.argv[2]
+
+ # Find matching JSON files across all providers
+ candidates: list[tuple[str, Path]] = []
+ for prov_dir in sorted(PROWLER_COMPLIANCE_DIR.iterdir()):
+ if not prov_dir.is_dir():
+ continue
+ for json_path in prov_dir.glob("*.json"):
+ stem = json_path.stem
+ if stem == framework_key or stem.startswith(f"{framework_key}_") \
+ or stem == f"{framework_key}_{prov_dir.name}":
+ candidates.append((prov_dir.name, json_path))
+
+ if not candidates:
+ print(f"No files matching '{framework_key}'", file=sys.stderr)
+ return 2
+
+ discovered_providers = sorted({prov for prov, _ in candidates})
+
+ by_id: dict[str, dict] = defaultdict(dict)
+ for prov, path in candidates:
+ with open(path) as f:
+ data = json.load(f)
+ for req in data["Requirements"]:
+ if req["Id"].startswith(id_prefix):
+ by_id[req["Id"]][prov] = {
+ "desc": req.get("Description", ""),
+ "sec": (req.get("Attributes") or [{}])[0].get("Section", ""),
+ "obj": (req.get("Attributes") or [{}])[0].get(
+ "SubSectionObjective", ""
+ ),
+ "checks": req.get("Checks") or [],
+ }
+
+ for ar_id in sorted(by_id):
+ rows = by_id[ar_id]
+ sample = next(iter(rows.values()))
+ print(f"\n### {ar_id}")
+ print(f" desc: {sample['desc']}")
+ if sample["sec"]:
+ print(f" sec : {sample['sec']}")
+ if sample["obj"]:
+ print(f" obj : {sample['obj']}")
+ for prov in discovered_providers:
+ if prov in rows:
+ checks = rows[prov]["checks"]
+ print(f" {prov}: ({len(checks)}) {checks}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/prowler-compliance/assets/parsers/__init__.py b/skills/prowler-compliance/assets/parsers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/skills/prowler-compliance/assets/parsers/finos_ccc.py b/skills/prowler-compliance/assets/parsers/finos_ccc.py
new file mode 100644
index 0000000000..a613b15857
--- /dev/null
+++ b/skills/prowler-compliance/assets/parsers/finos_ccc.py
@@ -0,0 +1,223 @@
+"""
+FINOS Common Cloud Controls (CCC) YAML parser.
+
+Reads cached upstream YAML files and emits Prowler-format requirements
+(``{Id, Description, Attributes: [...], Checks: []}``). This module is
+agnostic to providers, JSON output paths, framework metadata and legacy
+check-mapping preservation — those are handled by ``sync_framework.py``.
+
+Contract
+--------
+``parse_upstream(config: dict) -> list[dict]``
+ Returns a list of Prowler-format requirement dicts with **guaranteed
+ unique ids**. Foreign-prefix AR rewriting and genuine collision
+ renumbering both happen inside this module — the runner treats id
+ uniqueness as a contract violation, not as something to fix.
+
+Config keys consumed
+--------------------
+This parser reads the following config entries (the rest of the config is
+opaque to it):
+
+- ``upstream.dir`` — directory containing the cached YAMLs
+- ``parser.catalog_files`` — ordered list of YAML filenames to load
+- ``parser.family_id_title`` — suffix → canonical family title (shape 2)
+- ``parser.family_id_description`` — suffix → family description (shape 2)
+
+Upstream shapes
+---------------
+FINOS CCC catalogs come in two shapes:
+
+1. ``control-families: [{title, description, controls: [...]}]``
+ (used by most catalogs)
+2. ``controls: [{id, family: "CCC.X.Y", ...}]`` (no families wrapper; used
+ by ``storage/object``). The ``family`` field references a family id with
+ no human-readable title in the file — the title/description come from
+ ``config.parser.family_id_title`` / ``family_id_description``.
+
+Id rewriting rules
+------------------
+- **Foreign-prefix rewriting**: upstream intentionally aliases requirements
+ across catalogs by keeping the original prefix (e.g. ``CCC.AuditLog.CN08.AR01``
+ appears nested under ``CCC.Logging.CN03``). Prowler requires unique ids
+ within a catalog file, so we rename the AR to fit its parent control:
+ ``CCC.Logging.CN03.AR01``. See ``rewrite_ar_id()``.
+- **Genuine collision renumbering**: sometimes upstream has a real typo
+ where two distinct requirements share the same id (e.g.
+ ``CCC.Core.CN14.AR02`` appears twice for 30-day and 14-day backup variants).
+ The second copy is renumbered to the next free AR number within the
+ control. See the ``seen_ids`` logic in ``emit_requirement()``.
+"""
+from __future__ import annotations
+
+from pathlib import Path
+
+import yaml
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+
+def clean(value: str | None) -> str:
+ """Trim and collapse internal whitespace/newlines into single spaces.
+
+ Upstream YAML uses ``|`` block scalars that preserve newlines; Prowler
+ stores descriptions as single-line text.
+ """
+ if not value:
+ return ""
+ return " ".join(value.split())
+
+
+def flatten_mappings(mappings):
+ """Convert upstream ``{reference-id, entries: [{reference-id, ...}]}`` to
+ Prowler's ``{ReferenceId, Identifiers: [...]}``.
+ """
+ if not mappings:
+ return []
+ out = []
+ for m in mappings:
+ ids = []
+ for entry in m.get("entries") or []:
+ eid = entry.get("reference-id")
+ if eid:
+ ids.append(eid)
+ out.append({"ReferenceId": m.get("reference-id", ""), "Identifiers": ids})
+ return out
+
+
+def ar_prefix(ar_id: str) -> str:
+ """Return the first three dot-segments of an AR id (the parent control).
+
+ e.g. ``CCC.Core.CN01.AR01`` -> ``CCC.Core.CN01``.
+ """
+ return ".".join(ar_id.split(".")[:3])
+
+
+def rewrite_ar_id(parent_control_id: str, original_ar_id: str, ar_index: int) -> str:
+ """If an AR's id doesn't share its parent control's prefix, rename it.
+
+ Example
+ -------
+ parent ``CCC.Logging.CN03`` + AR id ``CCC.AuditLog.CN08.AR01`` with
+ index 0 -> ``CCC.Logging.CN03.AR01``.
+ """
+ if ar_prefix(original_ar_id) == parent_control_id:
+ return original_ar_id
+ return f"{parent_control_id}.AR{ar_index + 1:02d}"
+
+
+def emit_requirement(
+ control: dict,
+ family_name: str,
+ family_desc: str,
+ seen_ids: set[str],
+ requirements: list[dict],
+) -> None:
+ """Translate one FINOS control + its assessment-requirements into
+ Prowler-format requirement dicts and append them to ``requirements``.
+
+ Applies foreign-prefix rewriting and genuine-collision renumbering so
+ the final list is guaranteed to have unique ids.
+ """
+ control_id = clean(control.get("id"))
+ control_title = clean(control.get("title"))
+ section = f"{control_id} {control_title}".strip()
+ objective = clean(control.get("objective"))
+ threat_mappings = flatten_mappings(control.get("threat-mappings"))
+ guideline_mappings = flatten_mappings(control.get("guideline-mappings"))
+ ars = control.get("assessment-requirements") or []
+ for idx, ar in enumerate(ars):
+ raw_id = clean(ar.get("id"))
+ if not raw_id:
+ continue
+ new_id = rewrite_ar_id(control_id, raw_id, idx)
+ # Renumber on genuine upstream collision (find next free AR number)
+ if new_id in seen_ids:
+ base = ".".join(new_id.split(".")[:-1])
+ n = 1
+ while f"{base}.AR{n:02d}" in seen_ids:
+ n += 1
+ new_id = f"{base}.AR{n:02d}"
+ seen_ids.add(new_id)
+
+ requirements.append(
+ {
+ "Id": new_id,
+ "Description": clean(ar.get("text")),
+ "Attributes": [
+ {
+ "FamilyName": family_name,
+ "FamilyDescription": family_desc,
+ "Section": section,
+ "SubSection": "",
+ "SubSectionObjective": objective,
+ "Applicability": list(ar.get("applicability") or []),
+ "Recommendation": clean(ar.get("recommendation")),
+ "SectionThreatMappings": threat_mappings,
+ "SectionGuidelineMappings": guideline_mappings,
+ }
+ ],
+ "Checks": [],
+ }
+ )
+
+
+# ---------------------------------------------------------------------------
+# Public entry point
+# ---------------------------------------------------------------------------
+
+
+def parse_upstream(config: dict) -> list[dict]:
+ """Walk upstream YAMLs and emit Prowler-format requirements.
+
+ Handles both top-level shapes (``control-families`` and ``controls``).
+ Ids are guaranteed unique in the returned list.
+ """
+ upstream_dir = Path(config["upstream"]["dir"])
+ parser_cfg = config.get("parser") or {}
+ catalog_files = parser_cfg.get("catalog_files") or []
+ family_id_title = parser_cfg.get("family_id_title") or {}
+ family_id_description = parser_cfg.get("family_id_description") or {}
+
+ requirements: list[dict] = []
+ seen_ids: set[str] = set()
+
+ for filename in catalog_files:
+ path = upstream_dir / filename
+ if not path.exists():
+ # parser.catalog_files is the closed set of upstream catalogs
+ # that define the framework. Silently skipping a missing file
+ # would emit valid-looking JSON with part of the framework
+ # dropped, defeating the whole point of a canonical sync.
+ raise FileNotFoundError(
+ f"upstream catalog file not found: {path}\n"
+ f" hint: refresh the upstream cache (see SKILL.md Workflow A "
+ f"Step 1), or remove {filename!r} from parser.catalog_files "
+ f"if it has been retired upstream."
+ )
+ with open(path) as f:
+ doc = yaml.safe_load(f) or {}
+
+ # Shape 1: control-families wrapper
+ for family in doc.get("control-families") or []:
+ family_name = clean(family.get("title"))
+ family_desc = clean(family.get("description"))
+ for control in family.get("controls") or []:
+ emit_requirement(
+ control, family_name, family_desc, seen_ids, requirements
+ )
+
+ # Shape 2: top-level controls with family reference id
+ for control in doc.get("controls") or []:
+ family_ref = clean(control.get("family"))
+ suffix = family_ref.split(".")[-1] if family_ref else ""
+ family_name = family_id_title.get(suffix, suffix or "Data")
+ family_desc = family_id_description.get(suffix, "")
+ emit_requirement(
+ control, family_name, family_desc, seen_ids, requirements
+ )
+
+ return requirements
diff --git a/skills/prowler-compliance/assets/query_checks.py b/skills/prowler-compliance/assets/query_checks.py
new file mode 100644
index 0000000000..46405be982
--- /dev/null
+++ b/skills/prowler-compliance/assets/query_checks.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+"""
+Keyword/service/id lookup over a Prowler check inventory produced by
+build_inventory.py.
+
+Usage:
+ # Keyword AND-search across id + title + risk + description
+ python skills/prowler-compliance/assets/query_checks.py aws encryption transit
+
+ # Show all checks for a service
+ python skills/prowler-compliance/assets/query_checks.py aws --service iam
+
+ # Show full metadata for one check id
+ python skills/prowler-compliance/assets/query_checks.py aws --id kms_cmk_rotation_enabled
+"""
+from __future__ import annotations
+
+import json
+import sys
+
+
+def main() -> int:
+ if len(sys.argv) < 3:
+ print(__doc__)
+ return 1
+
+ provider = sys.argv[1]
+ try:
+ with open(f"/tmp/checks_{provider}.json") as f:
+ inv = json.load(f)
+ except FileNotFoundError:
+ print(
+ f"No inventory for {provider}. Run build_inventory.py first.",
+ file=sys.stderr,
+ )
+ return 2
+
+ if sys.argv[2] == "--service":
+ if len(sys.argv) < 4:
+ print("usage: --service ")
+ return 1
+ svc = sys.argv[3]
+ hits = [cid for cid in sorted(inv) if inv[cid].get("service") == svc]
+ for cid in hits:
+ print(f" {cid}")
+ print(f" {inv[cid].get('title', '')}")
+ print(f"\n{len(hits)} checks in service '{svc}'")
+ elif sys.argv[2] == "--id":
+ if len(sys.argv) < 4:
+ print("usage: --id ")
+ return 1
+ cid = sys.argv[3]
+ if cid not in inv:
+ print(f"NOT FOUND: {cid}")
+ return 3
+ m = inv[cid]
+ print(f"== {cid} ==")
+ print(f"service : {m.get('service')}")
+ print(f"severity: {m.get('severity')}")
+ print(f"resource: {m.get('resource')}")
+ print(f"title : {m.get('title')}")
+ print(f"desc : {m.get('description', '')[:500]}")
+ print(f"risk : {m.get('risk', '')[:500]}")
+ else:
+ keywords = [k.lower() for k in sys.argv[2:]]
+ hits = 0
+ for cid in sorted(inv):
+ m = inv[cid]
+ blob = " ".join(
+ [
+ cid,
+ m.get("title", ""),
+ m.get("risk", ""),
+ m.get("description", ""),
+ ]
+ ).lower()
+ if all(k in blob for k in keywords):
+ hits += 1
+ print(f" {cid} [{m.get('service', '')}]")
+ print(f" {m.get('title', '')[:120]}")
+ print(f"\n{hits} matches for {' + '.join(keywords)}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/prowler-compliance/assets/sync_framework.py b/skills/prowler-compliance/assets/sync_framework.py
new file mode 100644
index 0000000000..9e070f2691
--- /dev/null
+++ b/skills/prowler-compliance/assets/sync_framework.py
@@ -0,0 +1,536 @@
+#!/usr/bin/env python3
+"""
+Generic, config-driven compliance framework sync runner.
+
+Usage:
+ python skills/prowler-compliance/assets/sync_framework.py \
+ skills/prowler-compliance/assets/configs/ccc.yaml
+
+Pipeline:
+ 1. Load and validate the YAML config (fail fast on missing or empty
+ required fields — notably ``framework.version``, which silently
+ breaks ``get_check_compliance()`` key construction if empty).
+ 2. Dynamically import the parser module declared in ``parser.module``
+ (resolved as ``parsers.{name}`` under this script's directory).
+ 3. Call ``parser.parse_upstream(config) -> list[dict]`` to get raw
+ Prowler-format requirements. The parser owns all upstream-format
+ quirks (foreign-prefix AR rewriting, collision renumbering, shape
+ handling) and MUST return ids that are unique within the returned
+ list.
+ 4. **Safety net**: assert id uniqueness. The runner raises
+ ``ValueError`` on any duplicate — it does NOT silently renumber,
+ because mutating a canonical upstream id (e.g. CIS ``1.1.1`` or
+ NIST ``AC-2(1)``) would be catastrophic.
+ 5. Apply generic ``FamilyName`` normalization from
+ ``post_processing.family_name_normalization`` (optional).
+ 6. Preserve legacy ``Checks`` lists from the existing Prowler JSON
+ using a config-driven primary key + fallback key chain. CCC uses
+ ``(Section, Applicability)`` as fallback; CIS would use
+ ``(Section, Profile)``; NIST would use ``(ItemId,)``.
+ For versioned frameworks (e.g. ``cis__.json``)
+ where a version bump writes to a brand-new file, set
+ ``post_processing.check_preservation.legacy_path_template`` to
+ point at the previous version's file so its Checks are preserved
+ instead of silently lost. Defaults to ``output.path_template``
+ when omitted, which is correct for unversioned frameworks.
+ 7. Wrap each provider's requirements in the framework metadata dict
+ built from the config templates.
+ 8. Write each provider's JSON to the path resolved from
+ ``output.path_template`` (supports ``{framework}``, ``{version}``
+ and ``{provider}`` placeholders).
+ 9. Pydantic-validate the written JSON via ``Compliance.parse_file()``
+ and report the load counts per provider.
+
+The runner is strictly generic — it never mentions CCC, knows nothing
+about YAML shapes, and can handle any upstream-backed framework given a
+parser module and a config file.
+"""
+from __future__ import annotations
+
+import importlib
+import json
+import sys
+from pathlib import Path
+from typing import Any
+
+import yaml
+
+# Make sibling `parsers/` package importable regardless of the runner's
+# invocation directory.
+_SCRIPT_DIR = Path(__file__).resolve().parent
+if str(_SCRIPT_DIR) not in sys.path:
+ sys.path.insert(0, str(_SCRIPT_DIR))
+
+
+# ---------------------------------------------------------------------------
+# Config loading and validation
+# ---------------------------------------------------------------------------
+
+
+class ConfigError(ValueError):
+ """Raised when the sync config is malformed or missing required fields."""
+
+
+def _require(cfg: dict, dotted_path: str) -> Any:
+ """Fetch a dotted-path key from nested dicts. Raises ConfigError on
+ missing or empty values (empty-string, empty-list, None)."""
+ current: Any = cfg
+ parts = dotted_path.split(".")
+ for i, part in enumerate(parts):
+ if not isinstance(current, dict) or part not in current:
+ raise ConfigError(f"config: missing required field '{dotted_path}'")
+ current = current[part]
+ if current in ("", None, [], {}):
+ raise ConfigError(f"config: field '{dotted_path}' must not be empty")
+ return current
+
+
+def load_config(path: Path) -> dict:
+ if not path.exists():
+ raise ConfigError(f"config file not found: {path}")
+ with open(path) as f:
+ cfg = yaml.safe_load(f) or {}
+ if not isinstance(cfg, dict):
+ raise ConfigError(f"config root must be a mapping, got {type(cfg).__name__}")
+
+ # Required fields — fail fast. Empty Version in particular silently
+ # breaks get_check_compliance() key construction.
+ _require(cfg, "framework.name")
+ _require(cfg, "framework.display_name")
+ _require(cfg, "framework.version")
+ _require(cfg, "framework.description_template")
+ _require(cfg, "providers")
+ _require(cfg, "output.path_template")
+ _require(cfg, "upstream.dir")
+ _require(cfg, "parser.module")
+ _require(cfg, "post_processing.check_preservation.primary_key")
+
+ providers = cfg["providers"]
+ if not isinstance(providers, list) or not providers:
+ raise ConfigError("config: 'providers' must be a non-empty list")
+ for idx, p in enumerate(providers):
+ if not isinstance(p, dict) or "key" not in p or "display" not in p:
+ raise ConfigError(
+ f"config: providers[{idx}] must have 'key' and 'display' fields"
+ )
+
+ return cfg
+
+
+# ---------------------------------------------------------------------------
+# Parser loading
+# ---------------------------------------------------------------------------
+
+
+def load_parser(parser_module_name: str):
+ try:
+ return importlib.import_module(f"parsers.{parser_module_name}")
+ except ImportError as exc:
+ raise ConfigError(
+ f"cannot import parser 'parsers.{parser_module_name}': {exc}"
+ ) from exc
+
+
+# ---------------------------------------------------------------------------
+# Post-processing: id uniqueness safety net
+# ---------------------------------------------------------------------------
+
+
+def assert_unique_ids(requirements: list[dict]) -> None:
+ """Enforce the parser contract: every requirement must have a unique Id.
+
+ The runner never renumbers silently — a duplicate is a parser bug.
+ """
+ seen: set[str] = set()
+ dups: list[str] = []
+ for req in requirements:
+ rid = req.get("Id")
+ if not rid:
+ raise ValueError(f"requirement missing Id: {req}")
+ if rid in seen:
+ dups.append(rid)
+ seen.add(rid)
+ if dups:
+ raise ValueError(
+ f"parser returned duplicate requirement ids: {sorted(set(dups))}"
+ )
+
+
+# ---------------------------------------------------------------------------
+# Post-processing: FamilyName normalization
+# ---------------------------------------------------------------------------
+
+
+def normalize_family_names(requirements: list[dict], norm_map: dict[str, str]) -> None:
+ """Apply ``Attributes[0].FamilyName`` normalization in place."""
+ if not norm_map:
+ return
+ for req in requirements:
+ for attr in req.get("Attributes") or []:
+ name = attr.get("FamilyName")
+ if name in norm_map:
+ attr["FamilyName"] = norm_map[name]
+
+
+# ---------------------------------------------------------------------------
+# Post-processing: legacy check-mapping preservation
+# ---------------------------------------------------------------------------
+
+
+def _freeze(value: Any) -> Any:
+ """Make a value hashable for use in composite lookup keys.
+
+ Lists become frozensets (order-insensitive match). Scalars pass through.
+ """
+ if isinstance(value, list):
+ return frozenset(value)
+ return value
+
+
+def _build_fallback_key(attrs: dict, field_names: list[str]) -> tuple | None:
+ """Build a composite tuple key from the given attribute field names.
+
+ Returns None if any field is missing or falsy — that key will be
+ skipped (the lookup table just won't have an entry for it).
+ """
+ parts = []
+ for name in field_names:
+ if name not in attrs:
+ return None
+ value = attrs[name]
+ if value in ("", None, [], {}):
+ return None
+ parts.append(_freeze(value))
+ return tuple(parts)
+
+
+def load_legacy_check_maps(
+ legacy_path: Path,
+ primary_key: str,
+ fallback_keys: list[list[str]],
+) -> tuple[dict[str, list[str]], list[dict[tuple, list[str]]]]:
+ """Read the existing Prowler JSON and build lookup tables for check
+ preservation.
+
+ Fails fast on ambiguous preservation keys. If two distinct legacy
+ requirements share the same primary value or the same fallback tuple,
+ merging their ``Checks`` silently would corrupt the preserved mapping
+ for unrelated requirements. Raises ``ValueError`` listing every
+ conflict so the user can either dedupe the legacy data or strengthen
+ ``check_preservation`` in the sync config.
+
+ Returns
+ -------
+ by_primary : dict
+ ``{primary_value: [checks]}`` — e.g. ``{ar_id: [checks]}``.
+ by_fallback : list[dict]
+ One lookup dict per entry in ``fallback_keys``. Each maps a
+ composite tuple key to its preserved checks list.
+ """
+ by_primary: dict[str, list[str]] = {}
+ by_fallback: list[dict[tuple, list[str]]] = [{} for _ in fallback_keys]
+
+ if not legacy_path.exists():
+ return by_primary, by_fallback
+
+ with open(legacy_path) as f:
+ data = json.load(f)
+
+ # Track which legacy requirement Ids contributed to each bucket so we
+ # can surface ambiguity after the scan completes.
+ primary_sources: dict[str, list[str]] = {}
+ fallback_sources: list[dict[tuple, list[str]]] = [{} for _ in fallback_keys]
+
+ for req in data.get("Requirements") or []:
+ legacy_id = req.get("Id") or ""
+ checks = req.get("Checks") or []
+
+ pv = req.get(primary_key)
+ if pv:
+ primary_sources.setdefault(pv, []).append(legacy_id)
+ bucket = by_primary.setdefault(pv, [])
+ for c in checks:
+ if c not in bucket:
+ bucket.append(c)
+
+ attributes = req.get("Attributes") or []
+ if not attributes:
+ continue
+ attrs = attributes[0]
+ for i, field_names in enumerate(fallback_keys):
+ key = _build_fallback_key(attrs, field_names)
+ if key is None:
+ continue
+ fallback_sources[i].setdefault(key, []).append(legacy_id)
+ bucket = by_fallback[i].setdefault(key, [])
+ for c in checks:
+ if c not in bucket:
+ bucket.append(c)
+
+ conflicts: list[str] = []
+ for pv, ids in primary_sources.items():
+ if len(ids) > 1:
+ conflicts.append(
+ f"primary_key={primary_key!r} value={pv!r} shared by {ids}"
+ )
+ for i, field_names in enumerate(fallback_keys):
+ for key, ids in fallback_sources[i].items():
+ if len(ids) > 1:
+ conflicts.append(
+ f"fallback_key={field_names} value={key!r} shared by {ids}"
+ )
+ if conflicts:
+ details = "\n - ".join(conflicts)
+ raise ValueError(
+ f"ambiguous preservation keys in {legacy_path} — cannot "
+ f"faithfully preserve Checks across distinct requirements:\n"
+ f" - {details}\n"
+ f"Fix: dedupe the legacy JSON, or strengthen "
+ f"'post_processing.check_preservation' in the sync config "
+ f"(e.g. add a more discriminating field to fallback_keys)."
+ )
+
+ return by_primary, by_fallback
+
+
+def lookup_preserved_checks(
+ req: dict,
+ by_primary: dict,
+ by_fallback: list[dict],
+ primary_key: str,
+ fallback_keys: list[list[str]],
+) -> list[str]:
+ """Return preserved check ids for a requirement, trying the primary
+ key first then each fallback in order."""
+ pv = req.get(primary_key)
+ if pv and pv in by_primary:
+ return list(by_primary[pv])
+ attributes = req.get("Attributes") or []
+ if not attributes:
+ return []
+ attrs = attributes[0]
+ for i, field_names in enumerate(fallback_keys):
+ key = _build_fallback_key(attrs, field_names)
+ if key and key in by_fallback[i]:
+ return list(by_fallback[i][key])
+ return []
+
+
+# ---------------------------------------------------------------------------
+# Provider output assembly
+# ---------------------------------------------------------------------------
+
+
+def resolve_output_path(template: str, framework: dict, provider_key: str) -> Path:
+ return Path(
+ template.format(
+ provider=provider_key,
+ framework=framework["name"].lower(),
+ version=framework["version"],
+ )
+ )
+
+
+def build_provider_json(
+ config: dict,
+ provider: dict,
+ base_requirements: list[dict],
+) -> tuple[dict, dict[str, int]]:
+ """Produce the provider-specific JSON dict ready to dump.
+
+ Returns ``(json_dict, counts)`` where ``counts`` tracks how each
+ requirement's checks were resolved (primary, fallback, or none).
+ """
+ framework = config["framework"]
+ preservation = config["post_processing"]["check_preservation"]
+ primary_key = preservation["primary_key"]
+ fallback_keys = preservation.get("fallback_keys") or []
+
+ # For versioned frameworks, the file we WRITE (output.path_template
+ # resolved at the new version) is not the file we want to READ legacy
+ # Checks from. Allow the config to override the legacy source path so
+ # a version bump can still preserve mappings from the previous file.
+ legacy_template = (
+ preservation.get("legacy_path_template")
+ or config["output"]["path_template"]
+ )
+ legacy_path = resolve_output_path(
+ legacy_template, framework, provider["key"]
+ )
+ by_primary, by_fallback = load_legacy_check_maps(
+ legacy_path, primary_key, fallback_keys
+ )
+
+ counts = {"primary": 0, "fallback": 0, "none": 0}
+ enriched: list[dict] = []
+ for req in base_requirements:
+ # Try primary key first
+ pv = req.get(primary_key)
+ checks: list[str] = []
+ source = "none"
+ if pv and pv in by_primary:
+ checks = list(by_primary[pv])
+ source = "primary"
+ else:
+ attributes = req.get("Attributes") or []
+ if attributes:
+ attrs = attributes[0]
+ for i, field_names in enumerate(fallback_keys):
+ key = _build_fallback_key(attrs, field_names)
+ if key and key in by_fallback[i]:
+ checks = list(by_fallback[i][key])
+ source = "fallback"
+ break
+ counts[source] += 1
+ enriched.append(
+ {
+ "Id": req["Id"],
+ "Description": req["Description"],
+ # Shallow-copy attribute dicts so providers don't share refs
+ "Attributes": [dict(a) for a in req.get("Attributes") or []],
+ "Checks": checks,
+ }
+ )
+
+ description = framework["description_template"].format(
+ provider_display=provider["display"],
+ provider_key=provider["key"],
+ framework_name=framework["name"],
+ framework_display=framework["display_name"],
+ version=framework["version"],
+ )
+ out = {
+ "Framework": framework["name"],
+ "Version": framework["version"],
+ "Provider": provider["display"],
+ "Name": framework["display_name"],
+ "Description": description,
+ "Requirements": enriched,
+ }
+ return out, counts
+
+
+# ---------------------------------------------------------------------------
+# Pydantic post-validation
+# ---------------------------------------------------------------------------
+
+
+def pydantic_validate(json_path: Path) -> int:
+ """Import Prowler lazily so the runner still works without Prowler
+ installed (validation step is skipped in that case)."""
+ try:
+ from prowler.lib.check.compliance_models import Compliance
+ except ImportError:
+ print(
+ " note: prowler package not importable — skipping Pydantic validation",
+ file=sys.stderr,
+ )
+ return -1
+ try:
+ parsed = Compliance.parse_file(str(json_path))
+ except Exception as exc:
+ raise RuntimeError(
+ f"Pydantic validation failed for {json_path}: {exc}"
+ ) from exc
+ return len(parsed.Requirements)
+
+
+# ---------------------------------------------------------------------------
+# Driver
+# ---------------------------------------------------------------------------
+
+
+def main() -> int:
+ if len(sys.argv) != 2:
+ print("usage: sync_framework.py ", file=sys.stderr)
+ return 1
+
+ config_path = Path(sys.argv[1])
+ try:
+ config = load_config(config_path)
+ except ConfigError as exc:
+ print(f"config error: {exc}", file=sys.stderr)
+ return 2
+
+ framework_name = config["framework"]["name"]
+ upstream_dir = Path(config["upstream"]["dir"])
+ if not upstream_dir.exists():
+ print(
+ f"error: upstream cache dir {upstream_dir} not found\n"
+ f" hint: {config['upstream'].get('fetch_docs', '(see SKILL.md Workflow A Step 1)')}",
+ file=sys.stderr,
+ )
+ return 3
+
+ parser_module_name = config["parser"]["module"]
+ print(
+ f"Sync: framework={framework_name} version={config['framework']['version']} "
+ f"parser={parser_module_name}"
+ )
+
+ try:
+ parser = load_parser(parser_module_name)
+ except ConfigError as exc:
+ print(f"parser error: {exc}", file=sys.stderr)
+ return 4
+
+ print(f"Parsing upstream from {upstream_dir}...")
+ try:
+ base_requirements = parser.parse_upstream(config)
+ except FileNotFoundError as exc:
+ # A missing catalog declared in parser.catalog_files is a hard
+ # failure: emitting JSON with part of the framework silently
+ # dropped would violate the canonical-sync contract.
+ print(f"upstream error: {exc}", file=sys.stderr)
+ return 6
+ print(f" parser returned {len(base_requirements)} requirements")
+
+ # Safety-net: parser contract
+ try:
+ assert_unique_ids(base_requirements)
+ except ValueError as exc:
+ print(f"parser contract violation: {exc}", file=sys.stderr)
+ return 5
+
+ # Post-processing: family name normalization
+ norm_map = (
+ config.get("post_processing", {})
+ .get("family_name_normalization")
+ or {}
+ )
+ normalize_family_names(base_requirements, norm_map)
+
+ # Per-provider output
+ print()
+ for provider in config["providers"]:
+ provider_json, counts = build_provider_json(
+ config, provider, base_requirements
+ )
+ out_path = resolve_output_path(
+ config["output"]["path_template"],
+ config["framework"],
+ provider["key"],
+ )
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+ with open(out_path, "w") as f:
+ json.dump(provider_json, f, indent=2, ensure_ascii=False)
+ f.write("\n")
+
+ validated = pydantic_validate(out_path)
+ validated_msg = (
+ f" pydantic_reqs={validated}" if validated >= 0 else " pydantic=skipped"
+ )
+ print(
+ f" {provider['key']}: total={len(provider_json['Requirements'])} "
+ f"matched_primary={counts['primary']} "
+ f"matched_fallback={counts['fallback']} "
+ f"new_or_unmatched={counts['none']}{validated_msg}"
+ )
+ print(f" wrote {out_path}")
+
+ print("\nDone.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/setup.sh b/skills/setup.sh
index ec5512e8c5..c24706d718 100755
--- a/skills/setup.sh
+++ b/skills/setup.sh
@@ -1,10 +1,10 @@
#!/bin/bash
# Setup AI Skills for Prowler development
# Configures AI coding assistants that follow agentskills.io standard:
-# - Claude Code: .claude/skills/ symlink + CLAUDE.md copies
-# - Gemini CLI: .gemini/skills/ symlink + GEMINI.md copies
+# - Claude Code: .claude/skills/ symlink + CLAUDE.md symlink
+# - Gemini CLI: .gemini/skills/ symlink + GEMINI.md symlink
# - Codex (OpenAI): .codex/skills/ symlink + AGENTS.md (native)
-# - GitHub Copilot: .github/copilot-instructions.md copy
+# - GitHub Copilot: .github/copilot-instructions.md symlink
#
# Usage:
# ./setup.sh # Interactive mode (select AI assistants)
@@ -37,6 +37,28 @@ SETUP_COPILOT=false
# HELPER FUNCTIONS
# =============================================================================
+add_to_gitignore() {
+ local pattern="$1"
+ local gitignore_file="$REPO_ROOT/.gitignore"
+ local header="# AI Coding assistants assets"
+
+ # Create .gitignore if it doesn't exist
+ if [ ! -f "$gitignore_file" ]; then
+ touch "$gitignore_file"
+ fi
+
+ # Check if pattern exists (exact match or at end of file)
+ if ! grep -qxF "$pattern" "$gitignore_file"; then
+ # Check if header exists
+ if ! grep -qxF "$header" "$gitignore_file"; then
+ echo -e "\n\n$header" >> "$gitignore_file"
+ fi
+
+ echo "$pattern" >> "$gitignore_file"
+ echo -e "${GREEN} ✓ Added $pattern to .gitignore${NC}"
+ fi
+}
+
show_help() {
echo "Usage: $0 [OPTIONS]"
echo ""
@@ -109,6 +131,7 @@ setup_claude() {
if [ ! -d "$REPO_ROOT/.claude" ]; then
mkdir -p "$REPO_ROOT/.claude"
fi
+ add_to_gitignore ".claude/skills"
if [ -L "$target" ]; then
rm "$target"
@@ -119,8 +142,9 @@ setup_claude() {
ln -s "$SKILLS_SOURCE" "$target"
echo -e "${GREEN} ✓ .claude/skills -> skills/${NC}"
- # Copy AGENTS.md to CLAUDE.md
- copy_agents_md "CLAUDE.md"
+ # Link AGENTS.md to CLAUDE.md
+ link_agents_md "CLAUDE.md"
+ add_to_gitignore "CLAUDE.md"
}
setup_gemini() {
@@ -129,6 +153,7 @@ setup_gemini() {
if [ ! -d "$REPO_ROOT/.gemini" ]; then
mkdir -p "$REPO_ROOT/.gemini"
fi
+ add_to_gitignore ".gemini/skills"
if [ -L "$target" ]; then
rm "$target"
@@ -139,8 +164,9 @@ setup_gemini() {
ln -s "$SKILLS_SOURCE" "$target"
echo -e "${GREEN} ✓ .gemini/skills -> skills/${NC}"
- # Copy AGENTS.md to GEMINI.md
- copy_agents_md "GEMINI.md"
+ # Link AGENTS.md to GEMINI.md
+ link_agents_md "GEMINI.md"
+ add_to_gitignore "GEMINI.md"
}
setup_codex() {
@@ -149,6 +175,7 @@ setup_codex() {
if [ ! -d "$REPO_ROOT/.codex" ]; then
mkdir -p "$REPO_ROOT/.codex"
fi
+ add_to_gitignore ".codex/skills"
if [ -L "$target" ]; then
rm "$target"
@@ -164,12 +191,19 @@ setup_codex() {
setup_copilot() {
if [ -f "$REPO_ROOT/AGENTS.md" ]; then
mkdir -p "$REPO_ROOT/.github"
- cp "$REPO_ROOT/AGENTS.md" "$REPO_ROOT/.github/copilot-instructions.md"
+
+ # Link AGENTS.md -> .github/copilot-instructions.md
+ local target="$REPO_ROOT/.github/copilot-instructions.md"
+ ln -sf "../AGENTS.md" "$target"
+
echo -e "${GREEN} ✓ AGENTS.md -> .github/copilot-instructions.md${NC}"
+
+ # Add specifically the file, NOT the .github folder
+ add_to_gitignore ".github/copilot-instructions.md"
fi
}
-copy_agents_md() {
+link_agents_md() {
local target_name="$1"
local agents_files
local count=0
@@ -179,11 +213,15 @@ copy_agents_md() {
for agents_file in $agents_files; do
local agents_dir
agents_dir=$(dirname "$agents_file")
- cp "$agents_file" "$agents_dir/$target_name"
+
+ # Create relative symlink
+ # Since files are in same dir, we can just link to basename
+ (cd "$agents_dir" && ln -sf "$(basename "$agents_file")" "$target_name")
+
count=$((count + 1))
done
- echo -e "${GREEN} ✓ Copied $count AGENTS.md -> $target_name${NC}"
+ echo -e "${GREEN} ✓ Linked $count AGENTS.md -> $target_name${NC}"
}
# =============================================================================
@@ -302,4 +340,4 @@ echo "Configured:"
[ "$SETUP_COPILOT" = true ] && echo " • GitHub Copilot: .github/copilot-instructions.md"
echo ""
echo -e "${BLUE}Note: Restart your AI assistant to load the skills.${NC}"
-echo -e "${BLUE} AGENTS.md is the source of truth - edit it, then re-run this script.${NC}"
+echo -e "${BLUE} AGENTS.md is the source of truth - changes are reflected automatically via symlinks.${NC}"
diff --git a/skills/setup_test.sh b/skills/setup_test.sh
index c0e80afe99..db4749ed3f 100755
--- a/skills/setup_test.sh
+++ b/skills/setup_test.sh
@@ -201,40 +201,40 @@ test_symlink_not_created_without_flag() {
}
# =============================================================================
-# TESTS: AGENTS.md COPYING
+# TESTS: AGENTS.md LINKING
# =============================================================================
-test_copy_claude_agents_md() {
+test_link_claude_agents_md() {
run_setup --claude > /dev/null
- assert_file_exists "$TEST_DIR/CLAUDE.md" "Root CLAUDE.md should exist" && \
- assert_file_exists "$TEST_DIR/api/CLAUDE.md" "api/CLAUDE.md should exist" && \
- assert_file_exists "$TEST_DIR/ui/CLAUDE.md" "ui/CLAUDE.md should exist"
+ assert_symlink_exists "$TEST_DIR/CLAUDE.md" "Root CLAUDE.md should be a symlink" && \
+ assert_symlink_exists "$TEST_DIR/api/CLAUDE.md" "api/CLAUDE.md should be a symlink" && \
+ assert_symlink_exists "$TEST_DIR/ui/CLAUDE.md" "ui/CLAUDE.md should be a symlink"
}
-test_copy_gemini_agents_md() {
+test_link_gemini_agents_md() {
run_setup --gemini > /dev/null
- assert_file_exists "$TEST_DIR/GEMINI.md" "Root GEMINI.md should exist" && \
- assert_file_exists "$TEST_DIR/api/GEMINI.md" "api/GEMINI.md should exist" && \
- assert_file_exists "$TEST_DIR/ui/GEMINI.md" "ui/GEMINI.md should exist"
+ assert_symlink_exists "$TEST_DIR/GEMINI.md" "Root GEMINI.md should be a symlink" && \
+ assert_symlink_exists "$TEST_DIR/api/GEMINI.md" "api/GEMINI.md should be a symlink" && \
+ assert_symlink_exists "$TEST_DIR/ui/GEMINI.md" "ui/GEMINI.md should be a symlink"
}
-test_copy_copilot_to_github() {
+test_link_copilot_to_github() {
run_setup --copilot > /dev/null
- assert_file_exists "$TEST_DIR/.github/copilot-instructions.md" "Copilot instructions should exist"
+ assert_symlink_exists "$TEST_DIR/.github/copilot-instructions.md" "Copilot instructions should be a symlink"
}
-test_copy_codex_no_extra_files() {
+test_link_codex_no_extra_files() {
run_setup --codex > /dev/null
assert_file_not_exists "$TEST_DIR/CODEX.md" "CODEX.md should not be created"
}
-test_copy_not_created_without_flag() {
+test_link_not_created_without_flag() {
run_setup --codex > /dev/null
- assert_file_not_exists "$TEST_DIR/CLAUDE.md" "CLAUDE.md should not exist" && \
- assert_file_not_exists "$TEST_DIR/GEMINI.md" "GEMINI.md should not exist"
+ assert_symlink_not_exists "$TEST_DIR/CLAUDE.md" "CLAUDE.md should not exist" && \
+ assert_symlink_not_exists "$TEST_DIR/GEMINI.md" "GEMINI.md should not exist"
}
-test_copy_content_matches_source() {
+test_link_content_matches_source() {
run_setup --claude > /dev/null
local source_content target_content
source_content=$(cat "$TEST_DIR/AGENTS.md")
@@ -272,7 +272,7 @@ test_idempotent_multiple_runs() {
run_setup --claude > /dev/null
run_setup --claude > /dev/null
assert_symlink_exists "$TEST_DIR/.claude/skills" "Symlink should still exist after second run" && \
- assert_file_exists "$TEST_DIR/CLAUDE.md" "CLAUDE.md should still exist after second run"
+ assert_symlink_exists "$TEST_DIR/CLAUDE.md" "CLAUDE.md should still be a symlink after second run"
}
# =============================================================================
diff --git a/tests/lib/check/universal_compliance_models_test.py b/tests/lib/check/universal_compliance_models_test.py
new file mode 100644
index 0000000000..5a3e4aae56
--- /dev/null
+++ b/tests/lib/check/universal_compliance_models_test.py
@@ -0,0 +1,1118 @@
+import json
+import os
+
+import pytest
+from pydantic.v1 import ValidationError
+
+from prowler.lib.check.compliance_models import (
+ AttributeMetadata,
+ ChartConfig,
+ Compliance,
+ ComplianceFramework,
+ CriticalRequirementsFilter,
+ EnumValueDisplay,
+ I18nLabels,
+ OutputFormats,
+ OutputsConfig,
+ PDFConfig,
+ ReportFilter,
+ ScoringConfig,
+ ScoringFormula,
+ SplitByConfig,
+ TableConfig,
+ TableLabels,
+ UniversalComplianceRequirement,
+ adapt_legacy_to_universal,
+ load_compliance_framework_universal,
+)
+from tests.lib.outputs.compliance.fixtures import (
+ CIS_1_4_AWS,
+ ENS_RD2022_AWS,
+ KISA_ISMSP_AWS,
+ MITRE_ATTACK_AWS,
+ NIST_800_53_REVISION_4_AWS,
+ PROWLER_THREATSCORE_AWS,
+)
+
+
+class TestOutputFormats:
+ def test_defaults(self):
+ of = OutputFormats()
+ assert of.csv is True
+ assert of.ocsf is True
+
+ def test_explicit_false(self):
+ of = OutputFormats(csv=False, ocsf=False)
+ assert of.csv is False
+ assert of.ocsf is False
+
+
+class TestAttributeMetadata:
+ def test_basic(self):
+ meta = AttributeMetadata(key="Section", type="str")
+ assert meta.key == "Section"
+ assert meta.type == "str"
+ assert meta.output_formats.csv is True
+ assert meta.required is False
+
+ def test_with_enum(self):
+ meta = AttributeMetadata(
+ key="Profile",
+ type="str",
+ enum=["Level 1", "Level 2"],
+ )
+ assert meta.enum == ["Level 1", "Level 2"]
+
+ def test_int_type(self):
+ meta = AttributeMetadata(key="LevelOfRisk", type="int", required=True)
+ assert meta.type == "int"
+ assert meta.required is True
+
+ def test_enum_display_field(self):
+ meta = AttributeMetadata(
+ key="Dimensiones",
+ type="str",
+ enum=["confidencialidad", "integridad", "trazabilidad"],
+ enum_display={
+ "confidencialidad": {
+ "label": "Confidencialidad",
+ "abbreviation": "C",
+ "color": "#FF6347",
+ },
+ "integridad": {
+ "label": "Integridad",
+ "abbreviation": "I",
+ "color": "#4286F4",
+ },
+ "trazabilidad": {
+ "label": "Trazabilidad",
+ "abbreviation": "T",
+ "color": "#32CD32",
+ },
+ },
+ )
+ assert meta.enum_display is not None
+ assert meta.enum_display["confidencialidad"]["abbreviation"] == "C"
+ assert meta.enum_display["integridad"]["color"] == "#4286F4"
+
+ def test_enum_order_field(self):
+ meta = AttributeMetadata(
+ key="Nivel",
+ type="str",
+ enum=["opcional", "bajo", "medio", "alto"],
+ enum_order=["alto", "medio", "bajo", "opcional"],
+ )
+ assert meta.enum_order == ["alto", "medio", "bajo", "opcional"]
+
+ def test_chart_label_field(self):
+ meta = AttributeMetadata(
+ key="Section",
+ type="str",
+ chart_label="Security Domain",
+ )
+ assert meta.chart_label == "Security Domain"
+
+ def test_output_formats_default_true(self):
+ meta = AttributeMetadata(key="Section")
+ assert meta.output_formats.csv is True
+ assert meta.output_formats.ocsf is True
+
+ def test_output_formats_explicit_false(self):
+ meta = AttributeMetadata(
+ key="InternalNote",
+ output_formats=OutputFormats(csv=False, ocsf=False),
+ )
+ assert meta.output_formats.csv is False
+ assert meta.output_formats.ocsf is False
+
+ def test_new_fields_default_none(self):
+ meta = AttributeMetadata(key="Section")
+ assert meta.enum_display is None
+ assert meta.enum_order is None
+ assert meta.chart_label is None
+
+
+class TestEnumValueDisplay:
+ def test_basic(self):
+ evd = EnumValueDisplay(label="Test")
+ assert evd.label == "Test"
+ assert evd.abbreviation is None
+ assert evd.color is None
+ assert evd.icon is None
+
+ def test_dimension_style(self):
+ evd = EnumValueDisplay(
+ label="Trazabilidad",
+ abbreviation="T",
+ color="#4286F4",
+ )
+ assert evd.label == "Trazabilidad"
+ assert evd.abbreviation == "T"
+ assert evd.color == "#4286F4"
+
+ def test_tipo_style(self):
+ evd = EnumValueDisplay(
+ label="Requisito",
+ icon="⚠️",
+ )
+ assert evd.icon == "⚠️"
+ assert evd.abbreviation is None
+
+
+class TestChartConfig:
+ def test_horizontal_bar(self):
+ chart = ChartConfig(
+ id="section_compliance",
+ type="horizontal_bar",
+ group_by="Section",
+ title="Compliance Score by Domain",
+ y_label="Domain",
+ x_label="Compliance %",
+ )
+ assert chart.type == "horizontal_bar"
+ assert chart.group_by == "Section"
+ assert chart.value_source == "compliance_percent"
+ assert chart.color_mode == "by_value"
+
+ def test_vertical_bar(self):
+ chart = ChartConfig(
+ id="risk_distribution",
+ type="vertical_bar",
+ group_by="LevelOfRisk",
+ color_mode="fixed",
+ fixed_color="#336699",
+ )
+ assert chart.type == "vertical_bar"
+ assert chart.fixed_color == "#336699"
+
+ def test_radar(self):
+ chart = ChartConfig(
+ id="dimension_radar",
+ type="radar",
+ group_by="Dimensiones",
+ )
+ assert chart.type == "radar"
+
+ def test_defaults(self):
+ chart = ChartConfig(id="test", type="vertical_bar", group_by="Section")
+ assert chart.title is None
+ assert chart.x_label is None
+ assert chart.y_label is None
+ assert chart.value_source == "compliance_percent"
+ assert chart.color_mode == "by_value"
+ assert chart.fixed_color is None
+
+
+class TestScoringFormula:
+ def test_threatscore_style(self):
+ formula = ScoringFormula(
+ risk_field="LevelOfRisk",
+ weight_field="Weight",
+ risk_boost_factor=0.25,
+ )
+ assert formula.risk_field == "LevelOfRisk"
+ assert formula.weight_field == "Weight"
+ assert formula.risk_boost_factor == 0.25
+
+ def test_custom_boost_factor(self):
+ formula = ScoringFormula(
+ risk_field="Risk",
+ weight_field="Impact",
+ risk_boost_factor=0.5,
+ )
+ assert formula.risk_boost_factor == 0.5
+
+ def test_default_boost_factor(self):
+ formula = ScoringFormula(risk_field="LevelOfRisk", weight_field="Weight")
+ assert formula.risk_boost_factor == 0.25
+
+
+class TestCriticalRequirementsFilter:
+ def test_int_based(self):
+ crf = CriticalRequirementsFilter(
+ filter_field="LevelOfRisk",
+ min_value=4,
+ title="Critical Failed Requirements",
+ )
+ assert crf.filter_field == "LevelOfRisk"
+ assert crf.min_value == 4
+ assert crf.filter_value is None
+ assert crf.status_filter == "FAIL"
+ assert crf.title == "Critical Failed Requirements"
+
+ def test_string_based(self):
+ crf = CriticalRequirementsFilter(
+ filter_field="Nivel",
+ filter_value="alto",
+ )
+ assert crf.filter_value == "alto"
+ assert crf.min_value is None
+
+ def test_defaults(self):
+ crf = CriticalRequirementsFilter(filter_field="LevelOfRisk")
+ assert crf.status_filter == "FAIL"
+ assert crf.title is None
+ assert crf.min_value is None
+ assert crf.filter_value is None
+
+
+class TestReportFilter:
+ def test_defaults(self):
+ rf = ReportFilter()
+ assert rf.only_failed is True
+ assert rf.include_manual is False
+
+ def test_custom(self):
+ rf = ReportFilter(only_failed=False, include_manual=True)
+ assert rf.only_failed is False
+ assert rf.include_manual is True
+
+
+class TestI18nLabels:
+ def test_english_defaults(self):
+ labels = I18nLabels()
+ assert labels.page_label == "Page"
+ assert labels.powered_by == "Powered by Prowler"
+ assert labels.framework_label == "Framework:"
+ assert labels.provider_label == "Provider:"
+ assert labels.report_title is None
+
+ def test_spanish_override(self):
+ labels = I18nLabels(
+ report_title="Informe de Cumplimiento ENS",
+ page_label="Página",
+ powered_by="Generado por Prowler",
+ framework_label="Marco:",
+ version_label="Versión:",
+ provider_label="Proveedor:",
+ description_label="Descripción:",
+ compliance_score_label="Puntuación de Cumplimiento por Secciones",
+ requirements_index_label="Índice de Requisitos",
+ detailed_findings_label="Hallazgos Detallados",
+ )
+ assert labels.page_label == "Página"
+ assert labels.provider_label == "Proveedor:"
+ assert labels.report_title == "Informe de Cumplimiento ENS"
+
+
+class TestSplitByConfig:
+ def test_cis_style(self):
+ config = SplitByConfig(field="Profile", values=["Level 1", "Level 2"])
+ assert config.field == "Profile"
+ assert len(config.values) == 2
+
+ def test_ens_style(self):
+ config = SplitByConfig(
+ field="Nivel",
+ values=["alto", "medio", "bajo", "opcional"],
+ )
+ assert len(config.values) == 4
+
+
+class TestScoringConfig:
+ def test_threatscore_style(self):
+ config = ScoringConfig(risk_field="LevelOfRisk", weight_field="Weight")
+ assert config.risk_field == "LevelOfRisk"
+ assert config.weight_field == "Weight"
+
+
+class TestTableLabels:
+ def test_defaults(self):
+ labels = TableLabels()
+ assert labels.pass_label == "PASS"
+ assert labels.fail_label == "FAIL"
+ assert labels.provider_header == "Provider"
+
+ def test_ens_spanish(self):
+ labels = TableLabels(
+ pass_label="CUMPLE",
+ fail_label="NO CUMPLE",
+ provider_header="Proveedor",
+ )
+ assert labels.pass_label == "CUMPLE"
+
+
+class TestTableConfig:
+ def test_grouped_mode(self):
+ tc = TableConfig(group_by="Section")
+ assert tc.group_by == "Section"
+ assert tc.split_by is None
+ assert tc.scoring is None
+
+ def test_split_mode(self):
+ tc = TableConfig(
+ group_by="Section",
+ split_by=SplitByConfig(field="Profile", values=["Level 1", "Level 2"]),
+ )
+ assert tc.split_by is not None
+ assert tc.split_by.field == "Profile"
+
+ def test_scored_mode(self):
+ tc = TableConfig(
+ group_by="Section",
+ scoring=ScoringConfig(risk_field="LevelOfRisk", weight_field="Weight"),
+ )
+ assert tc.scoring is not None
+
+
+class TestPDFConfig:
+ def test_defaults(self):
+ pdf = PDFConfig()
+ assert pdf.language == "en"
+ assert pdf.logo_filename is None
+ assert pdf.primary_color is None
+ assert pdf.sections is None
+ assert pdf.section_short_names is None
+ assert pdf.group_by_field is None
+ assert pdf.sub_group_by_field is None
+ assert pdf.section_titles is None
+ assert pdf.charts is None
+ assert pdf.scoring is None
+ assert pdf.critical_filter is None
+ assert pdf.filter is None
+ assert pdf.labels is None
+
+ def test_csa_ccm_style(self):
+ pdf = PDFConfig(
+ primary_color="#336699",
+ secondary_color="#4D80B3",
+ bg_color="#F2F8FF",
+ group_by_field="Section",
+ sections=["Audit & Assurance", "Identity & Access Management"],
+ section_short_names={"Identity & Access Management": "IAM"},
+ charts=[
+ ChartConfig(
+ id="section_compliance",
+ type="horizontal_bar",
+ group_by="Section",
+ title="Compliance Score by Domain",
+ ).dict()
+ ],
+ filter=ReportFilter(only_failed=True, include_manual=False),
+ )
+ assert pdf.primary_color == "#336699"
+ assert len(pdf.sections) == 2
+ assert pdf.section_short_names["Identity & Access Management"] == "IAM"
+ assert pdf.group_by_field == "Section"
+ assert pdf.charts is not None
+ assert len(pdf.charts) == 1
+ assert pdf.filter.only_failed is True
+
+ def test_ens_style(self):
+ pdf = PDFConfig(
+ language="es",
+ logo_filename="ens_logo.png",
+ primary_color="#CC3333",
+ group_by_field="Marco",
+ sub_group_by_field="Categoria",
+ labels=I18nLabels(
+ page_label="Página",
+ provider_label="Proveedor:",
+ ),
+ )
+ assert pdf.language == "es"
+ assert pdf.logo_filename == "ens_logo.png"
+ assert pdf.group_by_field == "Marco"
+ assert pdf.sub_group_by_field == "Categoria"
+ assert pdf.labels.page_label == "Página"
+
+ def test_threatscore_style(self):
+ pdf = PDFConfig(
+ primary_color="#336699",
+ sections=["1. IAM", "2. Attack Surface"],
+ scoring=ScoringFormula(
+ risk_field="LevelOfRisk",
+ weight_field="Weight",
+ risk_boost_factor=0.25,
+ ),
+ critical_filter=CriticalRequirementsFilter(
+ filter_field="LevelOfRisk",
+ min_value=4,
+ title="Critical Failed Requirements",
+ ),
+ )
+ assert pdf.scoring is not None
+ assert pdf.scoring.risk_field == "LevelOfRisk"
+ assert pdf.critical_filter.min_value == 4
+
+ def test_section_titles(self):
+ pdf = PDFConfig(
+ section_titles={
+ "1": "1. Policy on Security",
+ "2": "2. Risk Management",
+ },
+ )
+ assert pdf.section_titles["1"] == "1. Policy on Security"
+
+ def test_in_framework(self):
+ fw = ComplianceFramework(
+ framework="Test",
+ name="Test Framework",
+ description="Test",
+ requirements=[],
+ outputs=OutputsConfig(
+ pdf_config=PDFConfig(
+ primary_color="#336699",
+ sections=["Section A"],
+ charts=[
+ ChartConfig(
+ id="test_chart",
+ type="vertical_bar",
+ group_by="Section",
+ ).dict()
+ ],
+ ),
+ ),
+ )
+ assert fw.outputs is not None
+ assert fw.outputs.pdf_config is not None
+ assert fw.outputs.pdf_config.primary_color == "#336699"
+ assert fw.outputs.pdf_config.sections == ["Section A"]
+ assert fw.outputs.pdf_config.charts is not None
+ assert len(fw.outputs.pdf_config.charts) == 1
+ assert fw.outputs.pdf_config.charts[0]["id"] == "test_chart"
+ assert fw.outputs.pdf_config.charts[0]["type"] == "vertical_bar"
+
+ def test_framework_without_pdf_config(self):
+ fw = ComplianceFramework(
+ framework="Test",
+ name="Test Framework",
+ description="Test",
+ requirements=[],
+ )
+ assert fw.outputs is None
+
+
+class TestUniversalComplianceRequirement:
+ def test_flat_dict_attributes(self):
+ req = UniversalComplianceRequirement(
+ id="1.1",
+ description="Test requirement",
+ attributes={"Section": "IAM", "Profile": "Level 1"},
+ checks={"aws": ["check_a", "check_b"]},
+ )
+ assert req.attributes["Section"] == "IAM"
+ assert len(req.checks["aws"]) == 2
+
+ def test_mitre_optional_fields(self):
+ req = UniversalComplianceRequirement(
+ id="T1190",
+ description="Exploit Public-Facing Application",
+ attributes={},
+ checks={"aws": ["drs_job_exist"]},
+ tactics=["Initial Access"],
+ sub_techniques=[],
+ platforms=["IaaS", "Linux"],
+ technique_url="https://attack.mitre.org/techniques/T1190/",
+ )
+ assert req.tactics == ["Initial Access"]
+ assert req.technique_url == "https://attack.mitre.org/techniques/T1190/"
+
+ def test_dict_checks_multi_provider(self):
+ req = UniversalComplianceRequirement(
+ id="1.1",
+ description="Multi-provider",
+ attributes={},
+ checks={"aws": ["check_a"], "azure": ["check_b"]},
+ )
+ assert isinstance(req.checks, dict)
+ assert "aws" in req.checks
+
+ def test_empty_checks(self):
+ req = UniversalComplianceRequirement(
+ id="manual-1",
+ description="Manual requirement",
+ attributes={"Section": "Governance"},
+ checks={},
+ )
+ assert req.checks == {}
+
+ def test_checks_default_is_empty_dict(self):
+ req = UniversalComplianceRequirement(
+ id="1.1",
+ description="No checks provided",
+ )
+ assert req.checks == {}
+
+
+class TestComplianceFramework:
+ def test_basic_framework(self):
+ fw = ComplianceFramework(
+ framework="TestFW",
+ name="Test Framework",
+ provider="AWS",
+ version="1.0",
+ description="A test framework",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="Test",
+ attributes={"Section": "IAM"},
+ checks={"aws": ["check_a"]},
+ )
+ ],
+ attributes_metadata=[
+ AttributeMetadata(key="Section", type="str"),
+ ],
+ outputs=OutputsConfig(table_config=TableConfig(group_by="Section")),
+ )
+ assert fw.framework == "TestFW"
+ assert fw.outputs.table_config.group_by == "Section"
+ assert len(fw.attributes_metadata) == 1
+ assert len(fw.requirements) == 1
+
+ def test_optional_provider(self):
+ fw = ComplianceFramework(
+ framework="MultiCloud",
+ name="Multi-cloud framework",
+ description="A multi-provider framework",
+ requirements=[],
+ )
+ assert fw.provider is None
+
+ def test_get_providers_from_dict_checks(self):
+ fw = ComplianceFramework(
+ framework="MultiCloud",
+ name="Multi-cloud",
+ description="test",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="test",
+ attributes={},
+ checks={
+ "aws": ["check_a"],
+ "azure": ["check_b"],
+ "gcp": ["check_c"],
+ },
+ ),
+ UniversalComplianceRequirement(
+ id="1.2",
+ description="test2",
+ attributes={},
+ checks={"aws": ["check_d"]},
+ ),
+ ],
+ )
+ providers = fw.get_providers()
+ assert providers == ["aws", "azure", "gcp"]
+
+ def test_get_providers_fallback_to_explicit(self):
+ fw = ComplianceFramework(
+ framework="SingleCloud",
+ name="Single-cloud",
+ provider="AWS",
+ description="test",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="test",
+ attributes={},
+ checks={},
+ ),
+ ],
+ )
+ providers = fw.get_providers()
+ assert providers == ["aws"]
+
+ def test_supports_provider_dict_checks(self):
+ fw = ComplianceFramework(
+ framework="MultiCloud",
+ name="Multi-cloud",
+ description="test",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="test",
+ attributes={},
+ checks={"aws": ["check_a"], "azure": ["check_b"]},
+ ),
+ ],
+ )
+ assert fw.supports_provider("aws") is True
+ assert fw.supports_provider("azure") is True
+ assert fw.supports_provider("gcp") is False
+
+ def test_supports_provider_explicit_only(self):
+ """Framework with explicit provider but no per-requirement checks still supports the provider."""
+ fw = ComplianceFramework(
+ framework="SingleCloud",
+ name="Single-cloud",
+ provider="AWS",
+ description="test",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="Manual requirement",
+ attributes={},
+ checks={},
+ ),
+ ],
+ )
+ assert fw.supports_provider("aws") is True
+ assert fw.supports_provider("azure") is False
+
+ def test_no_provider_field_with_dict_checks(self):
+ """Multi-provider JSON has no Provider field — providers derived from checks."""
+ fw = ComplianceFramework(
+ framework="CSA_CCM",
+ name="CSA CCM 4.0",
+ description="Cloud Controls Matrix",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="A&A-01",
+ description="Audit & Assurance",
+ attributes={"Domain": "A&A"},
+ checks={
+ "aws": ["check_a"],
+ "azure": ["check_b"],
+ "gcp": ["check_c"],
+ },
+ ),
+ ],
+ )
+ assert fw.provider is None
+ assert fw.get_providers() == ["aws", "azure", "gcp"]
+ assert fw.supports_provider("aws")
+ assert fw.supports_provider("azure")
+ assert fw.supports_provider("gcp")
+ assert not fw.supports_provider("kubernetes")
+
+ def test_icon_field(self):
+ fw = ComplianceFramework(
+ framework="CSA_CCM",
+ name="CSA CCM 4.0",
+ description="Cloud Controls Matrix",
+ icon="csa",
+ requirements=[],
+ )
+ assert fw.icon == "csa"
+
+ def test_icon_defaults_to_none(self):
+ fw = ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[],
+ )
+ assert fw.icon is None
+
+
+class TestAdaptLegacyToUniversal:
+ def test_adapt_cis(self):
+ fw = adapt_legacy_to_universal(CIS_1_4_AWS)
+ assert fw.framework == "CIS"
+ assert fw.provider == "AWS"
+ assert len(fw.requirements) == 2
+ # First requirement should have flat attributes
+ req = fw.requirements[0]
+ assert "Section" in req.attributes
+ assert req.attributes["Section"] == "2. Storage"
+ assert req.tactics is None
+ # Checks must be wrapped in dict keyed by provider
+ assert isinstance(req.checks, dict)
+ assert "aws" in req.checks
+
+ def test_adapt_ens(self):
+ fw = adapt_legacy_to_universal(ENS_RD2022_AWS)
+ assert fw.framework == "ENS"
+ req = fw.requirements[0]
+ assert "Marco" in req.attributes
+ assert req.attributes["Marco"] == "operacional"
+
+ def test_adapt_mitre(self):
+ fw = adapt_legacy_to_universal(MITRE_ATTACK_AWS)
+ assert fw.framework == "MITRE-ATTACK"
+ req = fw.requirements[0]
+ assert req.tactics == ["Initial Access"]
+ assert req.technique_url == "https://attack.mitre.org/techniques/T1190/"
+ assert "_raw_attributes" in req.attributes
+ assert isinstance(req.checks, dict)
+ assert "aws" in req.checks
+
+ def test_adapt_threatscore(self):
+ fw = adapt_legacy_to_universal(PROWLER_THREATSCORE_AWS)
+ req = fw.requirements[0]
+ assert req.attributes["LevelOfRisk"] == 5
+ assert req.attributes["Weight"] == 1000
+
+ def test_adapt_generic(self):
+ fw = adapt_legacy_to_universal(NIST_800_53_REVISION_4_AWS)
+ req = fw.requirements[0]
+ assert "Section" in req.attributes
+
+ def test_adapt_kisa(self):
+ fw = adapt_legacy_to_universal(KISA_ISMSP_AWS)
+ req = fw.requirements[0]
+ assert "Domain" in req.attributes
+
+ def test_inferred_metadata_cis(self):
+ fw = adapt_legacy_to_universal(CIS_1_4_AWS)
+ assert fw.attributes_metadata is not None
+ keys = [m.key for m in fw.attributes_metadata]
+ assert "Section" in keys
+ assert "Profile" in keys
+
+ def test_inferred_metadata_mitre_is_none(self):
+ fw = adapt_legacy_to_universal(MITRE_ATTACK_AWS)
+ assert fw.attributes_metadata is None
+
+ def test_table_config_is_none(self):
+ fw = adapt_legacy_to_universal(CIS_1_4_AWS)
+ assert fw.outputs is None
+
+
+class TestLoadComplianceFrameworkUniversal:
+ def test_load_universal_format(self, tmp_path):
+ data = {
+ "framework": "TestFW",
+ "name": "Test",
+ "provider": "AWS",
+ "version": "1.0",
+ "description": "desc",
+ "icon": "prowlerthreatscore",
+ "attributes_metadata": [{"key": "Section", "type": "str"}],
+ "outputs": {"table_config": {"group_by": "Section"}},
+ "requirements": [
+ {
+ "id": "1.1",
+ "description": "test",
+ "attributes": {"Section": "IAM"},
+ "checks": {"aws": ["check_a"]},
+ }
+ ],
+ }
+ path = tmp_path / "test.json"
+ path.write_text(json.dumps(data))
+ fw = load_compliance_framework_universal(str(path))
+ assert fw is not None
+ assert fw.framework == "TestFW"
+ assert fw.icon == "prowlerthreatscore"
+ assert fw.outputs.table_config.group_by == "Section"
+
+ def test_load_universal_multi_provider(self, tmp_path):
+ data = {
+ "framework": "CSA_CCM",
+ "name": "CSA CCM 4.0",
+ "version": "4.0",
+ "description": "Cloud Controls Matrix",
+ "attributes_metadata": [{"key": "Domain", "type": "str"}],
+ "outputs": {"table_config": {"group_by": "Domain"}},
+ "requirements": [
+ {
+ "id": "A&A-01",
+ "description": "Audit",
+ "attributes": {"Domain": "Audit"},
+ "checks": {
+ "aws": ["check_a"],
+ "azure": ["check_b"],
+ "gcp": ["check_c"],
+ },
+ }
+ ],
+ }
+ path = tmp_path / "csa_ccm_4.0.json"
+ path.write_text(json.dumps(data))
+ fw = load_compliance_framework_universal(str(path))
+ assert fw is not None
+ assert fw.provider is None
+ assert fw.get_providers() == ["aws", "azure", "gcp"]
+ assert fw.supports_provider("aws")
+ assert not fw.supports_provider("kubernetes")
+
+ def test_load_legacy_format(self, tmp_path):
+ data = {
+ "Framework": "SOC2",
+ "Name": "SOC2",
+ "Provider": "AWS",
+ "Version": "",
+ "Description": "desc",
+ "Requirements": [
+ {
+ "Id": "1.1",
+ "Description": "test",
+ "Attributes": [{"Section": "Access Control"}],
+ "Checks": ["check_a"],
+ }
+ ],
+ }
+ path = tmp_path / "legacy.json"
+ path.write_text(json.dumps(data))
+ fw = load_compliance_framework_universal(str(path))
+ assert fw is not None
+ assert fw.framework == "SOC2"
+ assert fw.outputs is None
+ assert fw.requirements[0].attributes["Section"] == "Access Control"
+ assert fw.requirements[0].checks == {"aws": ["check_a"]}
+
+
+class TestSmokeLoadAllJSONs:
+ """Parametrized smoke test: every existing compliance JSON must load as ComplianceFramework."""
+
+ @staticmethod
+ def _find_all_compliance_jsons():
+ base = os.path.join(
+ os.path.dirname(__file__),
+ "..",
+ "..",
+ "..",
+ "prowler",
+ "compliance",
+ )
+ base = os.path.normpath(base)
+ jsons = []
+ if os.path.isdir(base):
+ # Top-level JSONs (multi-provider)
+ for filename in os.listdir(base):
+ if filename.endswith(".json"):
+ jsons.append(os.path.join(base, filename))
+ # Provider sub-directory JSONs
+ for provider_dir in os.listdir(base):
+ provider_path = os.path.join(base, provider_dir)
+ if os.path.isdir(provider_path):
+ for filename in os.listdir(provider_path):
+ if filename.endswith(".json"):
+ jsons.append(os.path.join(provider_path, filename))
+ return jsons
+
+ @pytest.mark.parametrize(
+ "json_path",
+ _find_all_compliance_jsons.__func__(),
+ ids=lambda p: os.path.basename(p),
+ )
+ def test_loads_as_universal(self, json_path):
+ fw = load_compliance_framework_universal(json_path)
+ assert fw is not None, f"Failed to load {json_path}"
+ assert fw.framework
+ assert fw.name
+ assert len(fw.requirements) >= 0
+
+
+class TestBackwardCompat:
+ """Ensure Compliance.get_bulk still returns Compliance objects."""
+
+ def test_get_bulk_still_works(self):
+ # This test just validates the legacy path still returns Compliance objects
+ # We test with a constructed Compliance object
+ legacy = CIS_1_4_AWS
+ assert isinstance(legacy, Compliance)
+ assert legacy.Framework == "CIS"
+
+
+class TestAttributesMetadataValidation:
+ """Validate that Requirement attributes match their attributes_metadata schema."""
+
+ def _metadata(self, required=False, enum=None, type_str="str"):
+ return [
+ AttributeMetadata(key="Section", type="str", required=True),
+ AttributeMetadata(key="Level", type=type_str, required=required, enum=enum),
+ ]
+
+ def test_valid_attributes_pass(self):
+ fw = ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"Section": "IAM", "Level": "high"},
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(),
+ )
+ assert len(fw.requirements) == 1
+
+ def test_missing_required_key_raises(self):
+ with pytest.raises(
+ ValidationError, match="missing required attribute 'Section'"
+ ):
+ ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"Level": "high"},
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(),
+ )
+
+ def test_invalid_enum_value_raises(self):
+ with pytest.raises(ValidationError, match="not in"):
+ ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"Section": "IAM", "Level": "invalid"},
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(enum=["high", "low"]),
+ )
+
+ def test_valid_enum_value_passes(self):
+ fw = ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"Section": "IAM", "Level": "high"},
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(enum=["high", "low"]),
+ )
+ assert len(fw.requirements) == 1
+
+ def test_wrong_type_int_raises(self):
+ with pytest.raises(ValidationError, match="expected type int"):
+ ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"Section": "IAM", "Level": "not_a_number"},
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(type_str="int"),
+ )
+
+ def test_correct_type_int_passes(self):
+ fw = ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"Section": "IAM", "Level": 5},
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(type_str="int"),
+ )
+ assert fw.requirements[0].attributes["Level"] == 5
+
+ def test_none_optional_value_skips_validation(self):
+ """None values for non-required keys should not trigger type/enum errors."""
+ fw = ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"Section": "IAM", "Level": None},
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(enum=["high", "low"]),
+ )
+ assert len(fw.requirements) == 1
+
+ def test_no_metadata_skips_validation(self):
+ """Frameworks without attributes_metadata should not be validated."""
+ fw = ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"anything": "goes"},
+ checks={},
+ ),
+ ],
+ )
+ assert len(fw.requirements) == 1
+
+ def test_unknown_attribute_key_raises(self):
+ """Typos like 'Sectoin' must be rejected by the schema validator."""
+ with pytest.raises(ValidationError, match="unknown attribute 'Sectoin'"):
+ ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"Sectoin": "IAM", "Level": "high"},
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(enum=["high", "low"]),
+ )
+
+ def test_multiple_unknown_keys_all_reported(self):
+ """Every unknown key must appear in the validation error (deterministic order)."""
+ with pytest.raises(
+ ValidationError,
+ match=r"unknown attribute 'Bogus1'[\s\S]*unknown attribute 'Bogus2'",
+ ):
+ ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={
+ "Section": "IAM",
+ "Level": "high",
+ "Bogus1": "x",
+ "Bogus2": "y",
+ },
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(enum=["high", "low"]),
+ )
+
+ def test_multiple_errors_reported(self):
+ """All validation errors should be collected and reported together."""
+ with pytest.raises(
+ ValidationError, match="missing required attribute 'Section'"
+ ):
+ ComplianceFramework(
+ framework="Test",
+ name="Test",
+ description="d",
+ requirements=[
+ UniversalComplianceRequirement(
+ id="1.1",
+ description="d",
+ attributes={"Level": "bad"},
+ checks={},
+ ),
+ UniversalComplianceRequirement(
+ id="1.2",
+ description="d",
+ attributes={"Level": "also_bad"},
+ checks={},
+ ),
+ ],
+ attributes_metadata=self._metadata(enum=["high", "low"]),
+ )
diff --git a/tests/providers/aws/aws_provider_test.py b/tests/providers/aws/aws_provider_test.py
index 68342a2735..1a07ab13c9 100644
--- a/tests/providers/aws/aws_provider_test.py
+++ b/tests/providers/aws/aws_provider_test.py
@@ -839,6 +839,132 @@ aws:
assert isinstance(aws_provider, AwsProvider)
+ @mock_aws
+ def test_excluded_regions_removed_from_enabled_regions(self):
+ aws_provider = AwsProvider(excluded_regions={AWS_REGION_EU_WEST_1})
+
+ assert AWS_REGION_EU_WEST_1 not in aws_provider._enabled_regions
+ assert AWS_REGION_EU_WEST_1 not in aws_provider.generate_regional_clients("ec2")
+
+ @mock_aws
+ def test_excluded_regions_pruned_from_input_regions(self):
+ aws_provider = AwsProvider(
+ regions={AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1},
+ excluded_regions={AWS_REGION_EU_WEST_1},
+ )
+
+ assert AWS_REGION_EU_WEST_1 not in aws_provider._identity.audited_regions
+ assert AWS_REGION_US_EAST_1 in aws_provider._identity.audited_regions
+
+ @mock_aws
+ def test_excluded_regions_from_config_file(self):
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
+ tmp.write(f"aws:\n disallowed_regions:\n - {AWS_REGION_EU_WEST_1}\n")
+ config_path = tmp.name
+ try:
+ aws_provider = AwsProvider(config_path=config_path)
+ assert AWS_REGION_EU_WEST_1 not in aws_provider._enabled_regions
+ assert aws_provider._excluded_regions == {AWS_REGION_EU_WEST_1}
+ finally:
+ os.remove(config_path)
+
+ @mock_aws
+ def test_excluded_regions_from_env_on_direct_provider_init(self):
+ with mock.patch.dict(
+ os.environ,
+ {"PROWLER_AWS_DISALLOWED_REGIONS": AWS_REGION_EU_WEST_1},
+ clear=False,
+ ):
+ aws_provider = AwsProvider()
+
+ assert aws_provider._excluded_regions == {AWS_REGION_EU_WEST_1}
+ assert AWS_REGION_EU_WEST_1 not in aws_provider._enabled_regions
+
+ @mock_aws
+ def test_excluded_regions_precedence_explicit_over_env_and_config(self):
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
+ tmp.write(f"aws:\n disallowed_regions:\n - {AWS_REGION_EU_WEST_1}\n")
+ config_path = tmp.name
+ try:
+ with mock.patch.dict(
+ os.environ,
+ {"PROWLER_AWS_DISALLOWED_REGIONS": AWS_REGION_US_EAST_1},
+ clear=False,
+ ):
+ aws_provider = AwsProvider(
+ config_path=config_path,
+ excluded_regions={AWS_REGION_US_EAST_2},
+ )
+
+ assert aws_provider._excluded_regions == {AWS_REGION_US_EAST_2}
+ assert AWS_REGION_US_EAST_2 not in aws_provider._enabled_regions
+ assert AWS_REGION_EU_WEST_1 in aws_provider._enabled_regions
+ assert AWS_REGION_US_EAST_1 in aws_provider._enabled_regions
+ finally:
+ os.remove(config_path)
+
+ @mock_aws
+ def test_excluded_regions_from_config_avoid_excluded_profile_region(
+ self, monkeypatch
+ ):
+ monkeypatch.setenv("AWS_DEFAULT_REGION", AWS_REGION_EU_WEST_1)
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
+ tmp.write(f"aws:\n disallowed_regions:\n - {AWS_REGION_EU_WEST_1}\n")
+ config_path = tmp.name
+ try:
+ aws_provider = AwsProvider(config_path=config_path)
+
+ assert aws_provider.identity.profile_region == AWS_REGION_US_EAST_1
+ finally:
+ os.remove(config_path)
+
+ @mock_aws
+ def test_aws_provider_raises_when_all_input_regions_are_excluded(self):
+ with raises(AWSArgumentTypeValidationError):
+ AwsProvider(
+ regions={AWS_REGION_EU_WEST_1},
+ excluded_regions={AWS_REGION_EU_WEST_1},
+ )
+
+ def test_get_excluded_regions_from_env_parses_comma_list(self):
+ with mock.patch.dict(
+ os.environ,
+ {"PROWLER_AWS_DISALLOWED_REGIONS": " me-south-1 , ap-east-1 ,, "},
+ ):
+ assert Provider.get_excluded_regions_from_env() == {
+ "me-south-1",
+ "ap-east-1",
+ }
+
+ def test_get_excluded_regions_from_env_ignores_legacy_generic_name(self):
+ with mock.patch.dict(
+ os.environ,
+ {"PROWLER_DISALLOWED_REGIONS": "me-south-1"},
+ clear=True,
+ ):
+ assert Provider.get_excluded_regions_from_env() == set()
+
+ def test_get_excluded_regions_from_env_unset(self):
+ with mock.patch.dict(os.environ, {}, clear=True):
+ assert Provider.get_excluded_regions_from_env() == set()
+
+ @mock_aws
+ def test_print_credentials_shows_all_except_excluded_regions(self):
+ aws_provider = AwsProvider(
+ excluded_regions={AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1}
+ )
+
+ with patch(
+ "prowler.providers.aws.aws_provider.print_boxes"
+ ) as mock_print_boxes:
+ aws_provider.print_credentials()
+
+ report_lines = mock_print_boxes.call_args.args[0]
+ assert any(
+ "AWS Regions:" in line and "all except eu-west-1, us-east-1" in line
+ for line in report_lines
+ )
+
@mock_aws
def test_generate_regional_clients_all_enabled_regions(self):
aws_provider = AwsProvider()
@@ -2033,6 +2159,24 @@ aws:
== AWS_REGION_EU_WEST_1
)
+ def test_get_aws_region_for_sts_avoids_excluded_session_region(self):
+ input_regions = None
+ session_region = AWS_REGION_EU_WEST_1
+ assert (
+ get_aws_region_for_sts(
+ session_region, input_regions, {AWS_REGION_EU_WEST_1}
+ )
+ == AWS_REGION_US_EAST_1
+ )
+
+ def test_get_profile_region_avoids_excluded_session_region(self):
+ mocked_session = mock.Mock(region_name=AWS_REGION_EU_WEST_1)
+
+ assert (
+ AwsProvider.get_profile_region(mocked_session, {AWS_REGION_EU_WEST_1})
+ == AWS_REGION_US_EAST_1
+ )
+
@mock_aws
def test_set_session_config_default(self):
aws_provider = AwsProvider()
diff --git a/tests/providers/cloudflare/cloudflare_provider_test.py b/tests/providers/cloudflare/cloudflare_provider_test.py
index 58b7eb8086..c3ba6d75e2 100644
--- a/tests/providers/cloudflare/cloudflare_provider_test.py
+++ b/tests/providers/cloudflare/cloudflare_provider_test.py
@@ -433,6 +433,29 @@ class TestCloudflareValidateCredentials:
with pytest.raises(CloudflareNoAccountsError):
CloudflareProvider.validate_credentials(session)
+ def test_validate_credentials_breaks_on_repeated_account_ids(self):
+ """Pagination must stop when the SDK repeats account IDs to avoid infinite loops."""
+
+ def repeating_accounts():
+ account = MagicMock()
+ account.id = ACCOUNT_ID
+ while True:
+ yield account
+
+ mock_client = MagicMock()
+ mock_client.user.get.side_effect = Exception("Some other error")
+ mock_client.accounts.list.return_value = repeating_accounts()
+
+ session = CloudflareSession(
+ client=mock_client,
+ api_token=API_TOKEN,
+ api_key=None,
+ api_email=None,
+ )
+
+ # Must return without hanging; repeated IDs break the loop.
+ CloudflareProvider.validate_credentials(session)
+
class TestCloudflareTestConnection:
"""Tests for test_connection method."""
diff --git a/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py b/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py
index f367d5938d..0320ea4a82 100644
--- a/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py
+++ b/tests/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning_test.py
@@ -73,8 +73,8 @@ class TestCalendarExternalInvitationsWarning:
assert findings[0].status == "FAIL"
assert "disabled" in findings[0].status_extended
- def test_fail_no_policy_set(self):
- """Test FAIL when no explicit policy is set (None) but fetch succeeded"""
+ def test_pass_using_default(self):
+ """Test PASS when no explicit policy is set (None) — Google default is secure (enabled)"""
mock_provider = set_mocked_googleworkspace_provider()
with (
@@ -100,8 +100,8 @@ class TestCalendarExternalInvitationsWarning:
findings = check.execute()
assert len(findings) == 1
- assert findings[0].status == "FAIL"
- assert "not explicitly configured" in findings[0].status_extended
+ assert findings[0].status == "PASS"
+ assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
"""Test no findings returned when the API fetch failed"""
diff --git a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py
index 32056e9fcb..cb8713630f 100644
--- a/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py
+++ b/tests/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar_test.py
@@ -104,8 +104,8 @@ class TestCalendarExternalSharingPrimaryCalendar:
assert findings[0].status == "FAIL"
assert "EXTERNAL_ALL_INFO_READ_WRITE" in findings[0].status_extended
- def test_fail_no_policy_set(self):
- """Test FAIL when no explicit policy is set (None) but fetch succeeded"""
+ def test_pass_using_default(self):
+ """Test PASS when no explicit policy is set (None) — Google default is secure (free/busy only)"""
mock_provider = set_mocked_googleworkspace_provider()
with (
@@ -131,8 +131,8 @@ class TestCalendarExternalSharingPrimaryCalendar:
findings = check.execute()
assert len(findings) == 1
- assert findings[0].status == "FAIL"
- assert "not explicitly configured" in findings[0].status_extended
+ assert findings[0].status == "PASS"
+ assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
"""Test no findings returned when the API fetch failed"""
diff --git a/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py b/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py
index c9c7317f2b..2ad66b3d2f 100644
--- a/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py
+++ b/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py
@@ -67,8 +67,8 @@ class TestDriveExternalSharingWarnUsers:
assert findings[0].status == "FAIL"
assert "disabled" in findings[0].status_extended
- def test_fail_no_policy_set(self):
- """Test FAIL when no explicit policy is set (None) but fetch succeeded"""
+ def test_pass_using_default(self):
+ """Test PASS when no explicit policy is set (None) — Google default is secure"""
mock_provider = set_mocked_googleworkspace_provider()
with (
@@ -92,8 +92,8 @@ class TestDriveExternalSharingWarnUsers:
findings = check.execute()
assert len(findings) == 1
- assert findings[0].status == "FAIL"
- assert "not explicitly configured" in findings[0].status_extended
+ assert findings[0].status == "PASS"
+ assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
"""Test no findings returned when the API fetch failed"""
diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py
index f221e1ec75..016e643f2d 100644
--- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py
+++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py
@@ -69,8 +69,8 @@ class TestDriveSharedDriveCreationAllowed:
assert findings[0].status == "FAIL"
assert "prevented" in findings[0].status_extended
- def test_fail_no_policy_set(self):
- """Test FAIL when no explicit policy is set (None) but fetch succeeded"""
+ def test_pass_using_default(self):
+ """Test PASS when no explicit policy is set (None) — Google default is secure"""
mock_provider = set_mocked_googleworkspace_provider()
with (
@@ -94,8 +94,8 @@ class TestDriveSharedDriveCreationAllowed:
findings = check.execute()
assert len(findings) == 1
- assert findings[0].status == "FAIL"
- assert "not explicitly configured" in findings[0].status_extended
+ assert findings[0].status == "PASS"
+ assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
"""Test no findings returned when the API fetch failed"""
diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py
index 35ee7b1651..05edcb57a2 100644
--- a/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py
+++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py
@@ -101,8 +101,8 @@ class TestDriveSharedDriveDisableDownloadPrintCopy:
assert findings[0].status == "FAIL"
assert "ALL" in findings[0].status_extended
- def test_fail_no_policy_set(self):
- """Test FAIL when no explicit policy is set (None) but fetch succeeded"""
+ def test_pass_using_default(self):
+ """Test PASS when no explicit policy is set (None) — Google default is secure"""
mock_provider = set_mocked_googleworkspace_provider()
with (
@@ -128,8 +128,8 @@ class TestDriveSharedDriveDisableDownloadPrintCopy:
findings = check.execute()
assert len(findings) == 1
- assert findings[0].status == "FAIL"
- assert "not explicitly configured" in findings[0].status_extended
+ assert findings[0].status == "PASS"
+ assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
"""Test no findings returned when the API fetch failed"""
diff --git a/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py b/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py
index a494bbe2eb..1731b81fcc 100644
--- a/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py
+++ b/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py
@@ -71,8 +71,8 @@ class TestDriveWarnSharingWithAllowlistedDomains:
assert findings[0].status == "FAIL"
assert "disabled" in findings[0].status_extended
- def test_fail_no_policy_set(self):
- """Test FAIL when no explicit policy is set (None) but fetch succeeded"""
+ def test_pass_using_default(self):
+ """Test PASS when no explicit policy is set (None) — Google default is secure"""
mock_provider = set_mocked_googleworkspace_provider()
with (
@@ -98,8 +98,8 @@ class TestDriveWarnSharingWithAllowlistedDomains:
findings = check.execute()
assert len(findings) == 1
- assert findings[0].status == "FAIL"
- assert "not explicitly configured" in findings[0].status_extended
+ assert findings[0].status == "PASS"
+ assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
"""Test no findings returned when the API fetch failed"""
diff --git a/tests/providers/image/image_provider_test.py b/tests/providers/image/image_provider_test.py
index 941685f662..68b0c5d7a2 100644
--- a/tests/providers/image/image_provider_test.py
+++ b/tests/providers/image/image_provider_test.py
@@ -1185,3 +1185,58 @@ class TestInitGlobalProviderRegistryEnumeration:
# The "other/lib" repo should be filtered out by --image-filter
assert not any("other/lib" in img for img in provider.images)
assert len(provider.images) == 3
+
+
+class TestRegistryListMode:
+ """Regression test: `prowler image --registry --registry-list` crashes.
+
+ When --registry-list is passed, ImageProvider._enumerate_registry sets
+ _listing_only = True and __init__ returns early — before calling
+ Provider.set_global_provider(self). The caller in __main__.py then calls
+ global_provider.print_credentials() on a None reference, raising
+ AttributeError: 'NoneType' object has no attribute 'print_credentials'.
+ """
+
+ @patch("prowler.providers.image.image_provider.create_registry_adapter")
+ @patch("prowler.providers.common.provider.load_and_validate_config_file")
+ def test_registry_list_does_not_crash(self, mock_load_config, mock_adapter_factory):
+ """Reproduce the --registry-list crash by running the same sequence
+ as __main__.py: init_global_provider, get_global_provider,
+ then print_credentials."""
+ mock_load_config.return_value = {}
+
+ adapter = MagicMock()
+ adapter.list_repositories.return_value = ["myorg/app"]
+ adapter.list_tags.return_value = ["v1.0", "latest"]
+ mock_adapter_factory.return_value = adapter
+
+ arguments = Namespace(
+ provider="image",
+ config_file=None,
+ fixer_config=None,
+ images=None,
+ image_list_file=None,
+ scanners=["vuln"],
+ image_config_scanners=None,
+ trivy_severity=None,
+ ignore_unfixed=False,
+ timeout="5m",
+ registry="myregistry.io",
+ image_filter=None,
+ tag_filter=None,
+ max_images=0,
+ registry_insecure=False,
+ registry_list_images=True,
+ )
+
+ # Reproduce the exact crash sequence from __main__.py lines 289-294:
+ # Provider.init_global_provider(args)
+ # global_provider = Provider.get_global_provider()
+ # global_provider.print_credentials()
+ with mock.patch.object(Provider, "_global", None):
+ Provider.init_global_provider(arguments)
+ global_provider = Provider.get_global_provider()
+
+ # This is the line that crashes: global_provider is None so
+ # .print_credentials() raises AttributeError.
+ global_provider.print_credentials()
diff --git a/tests/providers/vercel/services/project/project_service_test.py b/tests/providers/vercel/services/project/project_service_test.py
new file mode 100644
index 0000000000..b82588ddd9
--- /dev/null
+++ b/tests/providers/vercel/services/project/project_service_test.py
@@ -0,0 +1,45 @@
+from unittest import mock
+
+from prowler.providers.vercel.services.project.project_service import Project
+from tests.providers.vercel.vercel_fixtures import (
+ PROJECT_ID,
+ PROJECT_NAME,
+ TEAM_ID,
+ set_mocked_vercel_provider,
+)
+
+
+class TestProjectService:
+ def test_list_projects_parses_security_metadata(self):
+ service = Project.__new__(Project)
+ service.provider = set_mocked_vercel_provider()
+ service.projects = {}
+ service._paginate = mock.MagicMock(
+ return_value=[
+ {
+ "id": PROJECT_ID,
+ "name": PROJECT_NAME,
+ "accountId": TEAM_ID,
+ "security": {
+ "firewallEnabled": True,
+ "firewallConfigVersion": 42,
+ "managedRules": {
+ "owasp": {"active": True, "action": "log"},
+ "ai_bots": {"active": False, "action": "deny"},
+ },
+ "botIdEnabled": True,
+ },
+ }
+ ]
+ )
+
+ service._list_projects()
+
+ project = service.projects[PROJECT_ID]
+ assert project.firewall_enabled is True
+ assert project.firewall_config_version == "42"
+ assert project.managed_rules == {
+ "owasp": {"active": True, "action": "log"},
+ "ai_bots": {"active": False, "action": "deny"},
+ }
+ assert project.bot_id_enabled is True
diff --git a/tests/providers/vercel/services/security/security_service_test.py b/tests/providers/vercel/services/security/security_service_test.py
new file mode 100644
index 0000000000..d0a690b1db
--- /dev/null
+++ b/tests/providers/vercel/services/security/security_service_test.py
@@ -0,0 +1,199 @@
+from unittest import mock
+
+from prowler.providers.vercel.services.project.project_service import VercelProject
+from prowler.providers.vercel.services.security.security_service import Security
+from tests.providers.vercel.vercel_fixtures import PROJECT_ID, PROJECT_NAME, TEAM_ID
+
+
+class TestSecurityService:
+ def test_fetch_firewall_config_reads_active_version_and_normalizes_response(self):
+ project = VercelProject(id=PROJECT_ID, name=PROJECT_NAME, team_id=TEAM_ID)
+ service = Security.__new__(Security)
+ service.firewall_configs = {}
+
+ service._get = mock.MagicMock(
+ return_value={
+ "active": {
+ "firewallEnabled": True,
+ "managedRules": {
+ "owasp": {"active": True, "action": "deny"},
+ "ai_bots": {"active": False, "action": "deny"},
+ },
+ "rules": [
+ {
+ "id": "rule-custom",
+ "name": "Block admin access",
+ "active": True,
+ "conditionGroup": [
+ {
+ "conditions": [
+ {
+ "type": "path",
+ "op": "pre",
+ "value": "/admin",
+ }
+ ]
+ }
+ ],
+ "action": {
+ "mitigate": {
+ "action": "deny",
+ }
+ },
+ },
+ {
+ "id": "rule-rate-limit",
+ "name": "Rate limit login",
+ "active": True,
+ "conditionGroup": [
+ {
+ "conditions": [
+ {
+ "type": "path",
+ "op": "eq",
+ "value": "/login",
+ }
+ ]
+ }
+ ],
+ "action": {
+ "mitigate": {
+ "action": "deny",
+ "rateLimit": {
+ "algo": "fixed_window",
+ "window": 60,
+ "limit": 10,
+ },
+ }
+ },
+ },
+ ],
+ "ips": [
+ {
+ "id": "ip-rule",
+ "ip": "203.0.113.7",
+ "action": "deny",
+ }
+ ],
+ },
+ "draft": None,
+ "versions": [1],
+ }
+ )
+
+ service._fetch_firewall_config(project)
+
+ service._get.assert_called_once_with(
+ "/v1/security/firewall/config/active",
+ params={"projectId": PROJECT_ID, "teamId": TEAM_ID},
+ )
+
+ config = service.firewall_configs[PROJECT_ID]
+ assert config.firewall_enabled is True
+ assert config.managed_rulesets == {"owasp": {"active": True, "action": "deny"}}
+ assert [rule["id"] for rule in config.custom_rules] == ["rule-custom"]
+ assert [rule["id"] for rule in config.rate_limiting_rules] == [
+ "rule-rate-limit"
+ ]
+ assert [rule["id"] for rule in config.ip_blocking_rules] == ["ip-rule"]
+
+ def test_fetch_firewall_config_parses_crs_managed_rulesets(self):
+ project = VercelProject(
+ id=PROJECT_ID,
+ name=PROJECT_NAME,
+ team_id=TEAM_ID,
+ firewall_config_version="1",
+ )
+ service = Security.__new__(Security)
+ service.firewall_configs = {}
+
+ service._get = mock.MagicMock(
+ return_value={
+ "id": "waf_test",
+ "version": 1,
+ "firewallEnabled": True,
+ "crs": {
+ "gen": {"active": True, "action": "log"},
+ "xss": {"active": True, "action": "deny"},
+ "php": {"active": False, "action": "log"},
+ },
+ "rules": [],
+ "ips": [],
+ }
+ )
+
+ service._fetch_firewall_config(project)
+
+ config = service.firewall_configs[PROJECT_ID]
+ assert config.firewall_enabled is True
+ assert config.managed_rulesets == {
+ "gen": {"active": True, "action": "log"},
+ "xss": {"active": True, "action": "deny"},
+ }
+
+ def test_fetch_firewall_config_falls_back_to_wrapper_when_active_missing(self):
+ project = VercelProject(id=PROJECT_ID, name=PROJECT_NAME, team_id=TEAM_ID)
+ service = Security.__new__(Security)
+ service.firewall_configs = {}
+
+ service._get = mock.MagicMock(
+ side_effect=[
+ Exception("404 active config not found"),
+ {"active": None, "draft": None, "versions": []},
+ ]
+ )
+
+ service._fetch_firewall_config(project)
+
+ assert service._get.call_args_list == [
+ mock.call(
+ "/v1/security/firewall/config/active",
+ params={"projectId": PROJECT_ID, "teamId": TEAM_ID},
+ ),
+ mock.call(
+ "/v1/security/firewall/config",
+ params={"projectId": PROJECT_ID, "teamId": TEAM_ID},
+ ),
+ ]
+
+ config = service.firewall_configs[PROJECT_ID]
+ assert config.firewall_enabled is False
+ assert config.managed_rulesets == {}
+ assert config.custom_rules == []
+ assert config.rate_limiting_rules == []
+ assert config.ip_blocking_rules == []
+
+ def test_fetch_firewall_config_uses_project_security_metadata_when_config_empty(
+ self,
+ ):
+ project = VercelProject(
+ id=PROJECT_ID,
+ name=PROJECT_NAME,
+ team_id=TEAM_ID,
+ firewall_enabled=True,
+ firewall_config_version="42",
+ managed_rules={
+ "owasp": {"active": True, "action": "log"},
+ "ai_bots": {"active": False, "action": "deny"},
+ },
+ )
+ service = Security.__new__(Security)
+ service.firewall_configs = {}
+
+ service._get = mock.MagicMock(
+ return_value={"active": None, "draft": None, "versions": []}
+ )
+
+ service._fetch_firewall_config(project)
+
+ service._get.assert_called_once_with(
+ "/v1/security/firewall/config/42",
+ params={"projectId": PROJECT_ID, "teamId": TEAM_ID},
+ )
+
+ config = service.firewall_configs[PROJECT_ID]
+ assert config.firewall_enabled is True
+ assert config.managed_rulesets == {"owasp": {"active": True, "action": "log"}}
+ assert config.custom_rules == []
+ assert config.rate_limiting_rules == []
+ assert config.ip_blocking_rules == []
diff --git a/ui/.dockerignore b/ui/.dockerignore
index 54ed258b8c..9049298c54 100644
--- a/ui/.dockerignore
+++ b/ui/.dockerignore
@@ -13,5 +13,5 @@ README.md
!.next/static
!.next/standalone
.git
-.husky
+.pre-commit-config.yaml
scripts/setup-git-hooks.js
diff --git a/ui/.pre-commit-config.yaml b/ui/.pre-commit-config.yaml
new file mode 100644
index 0000000000..c92ea6cfd3
--- /dev/null
+++ b/ui/.pre-commit-config.yaml
@@ -0,0 +1,37 @@
+orphan: true
+
+repos:
+ - repo: local
+ hooks:
+ - id: ui-typecheck
+ name: UI - TypeScript Check
+ entry: pnpm run typecheck
+ language: system
+ files: '\.(ts|tsx|js|jsx)$'
+ pass_filenames: false
+ priority: 0
+
+ - id: ui-lint
+ name: UI - ESLint
+ entry: pnpm run lint:check
+ language: system
+ files: '\.(ts|tsx|js|jsx)$'
+ pass_filenames: false
+ priority: 0
+
+ - id: ui-tests
+ name: UI - Unit Tests
+ entry: pnpm exec vitest related --run
+ language: system
+ files: '\.(ts|tsx|js|jsx)$'
+ exclude: '\.test\.|\.spec\.|vitest\.config|vitest\.setup'
+ pass_filenames: true
+ priority: 1
+
+ - id: ui-build
+ name: UI - Build
+ entry: pnpm run build
+ language: system
+ files: '\.(ts|tsx|js|jsx|json|css)$'
+ pass_filenames: false
+ priority: 2
diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md
index ded143758e..d4f378adf8 100644
--- a/ui/CHANGELOG.md
+++ b/ui/CHANGELOG.md
@@ -2,16 +2,51 @@
All notable changes to the **Prowler UI** are documented in this file.
-## [1.24.0] (Prowler UNRELEASED)
+## [1.25.0] (Prowler UNRELEASED)
+
+### 🔄 Changed
+
+- Redesign compliance page with a horizontal ThreatScore card (always-visible pillar breakdown + ActionDropdown), client-side search for compliance frameworks, compact scan selector trigger, responsive mobile filters, download-started toasts for CSV/PDF exports, enhanced compliance cards with truncated titles, and Alert-based empty/error states; migrate Progress component from HeroUI to shadcn [(#10767)](https://github.com/prowler-cloud/prowler/pull/10767)
+- Backward-compatibility middleware redirect from `/sign-up?invitation_token=…` to `/invitation/accept?invitation_token=…`; new invitation emails use `/invitation/accept` directly [(#10797)](https://github.com/prowler-cloud/prowler/pull/10797)
+
+---
+
+## [1.24.1] (Prowler v5.24.1)
+
+### 🐞 Fixed
+
+- Findings and filter UX fixes: exclude muted findings by default in the resource detail drawer and finding group resource views, show category context label (for example `Status: FAIL`) on MultiSelect triggers instead of hiding the placeholder, and add a `wide` width option for filter dropdowns applied to the findings Scan filter to prevent label truncation [(#10734)](https://github.com/prowler-cloud/prowler/pull/10734)
+- Findings grouped view now handles zero-resource IaC counters, refines drawer loading states, and adds provider indicators to finding groups [(#10736)](https://github.com/prowler-cloud/prowler/pull/10736)
+- Other Findings for this resource: ordering by `severity` [(#10778)](https://github.com/prowler-cloud/prowler/pull/10778)
+- Other Findings for this resource: show `delta` indicator [(#10778)](https://github.com/prowler-cloud/prowler/pull/10778)
+- Compliance: requirement findings do not show muted findings [(#10778)](https://github.com/prowler-cloud/prowler/pull/10778)
+- Latest new findings: link to finding groups order by `-severity,-last_seen_at` [(#10778)](https://github.com/prowler-cloud/prowler/pull/10778)
+
+### 🔒 Security
+
+- Upgrade React to 19.2.5 and Next.js to 16.2.3 to mitigate CVE-2026-23869 (React2DoS), a high-severity unauthenticated remote DoS vulnerability in the React Flight Protocol's Server Function deserialization [(#10754)](https://github.com/prowler-cloud/prowler/pull/10754)
+
+---
+
+## [1.24.0] (Prowler v5.24.0)
### 🚀 Added
- Resources side drawer with redesigned detail panel [(#10673)](https://github.com/prowler-cloud/prowler/pull/10673)
+- Syntax highlighting for remediation code blocks in finding groups drawer with provider-aware auto-detection (Shell, HCL, YAML, Bicep) [(#10698)](https://github.com/prowler-cloud/prowler/pull/10698)
+
+### 🔄 Changed
+
+- Attack Paths scan selection: contextual button labels based on graph availability, tooltips on disabled actions, green dot indicator for selectable scans, and a warning banner when viewing data from a previous scan cycle [(#10685)](https://github.com/prowler-cloud/prowler/pull/10685)
+- Remove legacy finding detail sheet, row-details wrapper, and resource detail panel; unify findings and resources around new side drawers [(#10692)](https://github.com/prowler-cloud/prowler/pull/10692)
+- Attack Paths "View Finding" now opens the finding drawer inline over the graph instead of navigating to `/findings` in a new tab, preserving graph zoom, selection, and filter state
+- Attack Paths scan table: replace action buttons with radio buttons, add dedicated Graph column, use info-colored In Progress badge, remove redundant Progress column, and fix info banner variant [(#10704)](https://github.com/prowler-cloud/prowler/pull/10704)
### 🐞 Fixed
- Findings group resource filters now strip unsupported scan parameters, display scan name instead of provider alias in filter badges, migrate mute modal from HeroUI to shadcn, and add searchable accounts/provider type selectors [(#10662)](https://github.com/prowler-cloud/prowler/pull/10662)
- Compliance detail page header now reflects the actual provider, alias and UID of the selected scan instead of always defaulting to AWS [(#10674)](https://github.com/prowler-cloud/prowler/pull/10674)
+- Provider wizard modal moved to a stable page-level host so the providers table refreshes after link, authenticate, and connection check without closing the modal [(#10675)](https://github.com/prowler-cloud/prowler/pull/10675)
---
@@ -40,7 +75,6 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🐞 Fixed
- Preserve query parameters in callbackUrl during invitation flow [(#10571)](https://github.com/prowler-cloud/prowler/pull/10571)
-- Deleting the active organization now switches to the target org before deleting, preventing JWT rejection from the backend [(#10491)](https://github.com/prowler-cloud/prowler/pull/10491)
- Clear Filters now resets all filters including muted findings and auto-applies, Clear all in pills only removes pill-visible sub-filters, and the discard icon is now an Undo text button [(#10446)](https://github.com/prowler-cloud/prowler/pull/10446)
- Send to Jira modal now dynamically fetches and displays available issue types per project instead of hardcoding `"Task"`, fixing failures on non-English Jira instances [(#10534)](https://github.com/prowler-cloud/prowler/pull/10534)
- Exclude service filter from finding group resources endpoint to prevent empty results when a service filter is active [(#10652)](https://github.com/prowler-cloud/prowler/pull/10652)
diff --git a/ui/README.md b/ui/README.md
index 148ef61d5b..b1af73ffe3 100644
--- a/ui/README.md
+++ b/ui/README.md
@@ -85,10 +85,10 @@ git clone git@github.com:prowler-cloud/ui.git
pnpm install
```
-**Note:** The `pnpm install` command will automatically configure Git hooks for code quality checks. If you experience issues, you can manually configure them:
+**Note:** The `pnpm install` command will automatically configure prek Git hooks for code quality checks. If hooks are not installed, run from the repo root:
```bash
-git config core.hooksPath "ui/.husky"
+prek install
```
#### Run the development server
diff --git a/ui/actions/compliances/compliances.ts b/ui/actions/compliances/compliances.ts
index b23723f670..d5f4fd4954 100644
--- a/ui/actions/compliances/compliances.ts
+++ b/ui/actions/compliances/compliances.ts
@@ -6,12 +6,10 @@ import { handleApiResponse } from "@/lib/server-actions-helper";
export const getCompliancesOverview = async ({
scanId,
region,
- query,
filters = {},
}: {
scanId?: string;
region?: string | string[];
- query?: string;
filters?: Record;
} = {}) => {
const headers = await getAuthHeaders({ contentType: false });
@@ -31,8 +29,6 @@ export const getCompliancesOverview = async ({
setParam("filter[scan_id]", scanId);
setParam("filter[region__in]", region);
- if (query) url.searchParams.set("filter[search]", query);
-
try {
const response = await fetch(url.toString(), {
headers,
@@ -46,15 +42,16 @@ export const getCompliancesOverview = async ({
};
export const getComplianceOverviewMetadataInfo = async ({
- query = "",
sort = "",
filters = {},
-}) => {
+}: {
+ sort?: string;
+ filters?: Record;
+} = {}) => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/compliance-overviews/metadata`);
- if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
diff --git a/ui/actions/finding-groups/finding-groups.test.ts b/ui/actions/finding-groups/finding-groups.test.ts
index f71fe989fb..709469b416 100644
--- a/ui/actions/finding-groups/finding-groups.test.ts
+++ b/ui/actions/finding-groups/finding-groups.test.ts
@@ -70,7 +70,7 @@ describe("getFindingGroups — default sort for muted and non-muted rows", () =>
const calledUrl = fetchMock.mock.calls[0][0] as string;
const url = new URL(calledUrl);
expect(url.searchParams.get("sort")).toBe(
- "-status,-new_fail_count,-changed_fail_count,-severity,-fail_count,-last_seen_at",
+ "-status,-severity,-new_fail_count,-changed_fail_count,-fail_count,-last_seen_at",
);
});
@@ -84,7 +84,7 @@ describe("getFindingGroups — default sort for muted and non-muted rows", () =>
const calledUrl = fetchMock.mock.calls[0][0] as string;
const url = new URL(calledUrl);
expect(url.searchParams.get("sort")).toBe(
- "-status,-new_fail_count,-changed_fail_count,-severity,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at",
+ "-status,-severity,-new_fail_count,-changed_fail_count,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at",
);
});
});
@@ -106,7 +106,7 @@ describe("getLatestFindingGroups — default sort for muted and non-muted rows",
const calledUrl = fetchMock.mock.calls[0][0] as string;
const url = new URL(calledUrl);
expect(url.searchParams.get("sort")).toBe(
- "-status,-new_fail_count,-changed_fail_count,-severity,-fail_count,-last_seen_at",
+ "-status,-severity,-new_fail_count,-changed_fail_count,-fail_count,-last_seen_at",
);
});
@@ -120,7 +120,7 @@ describe("getLatestFindingGroups — default sort for muted and non-muted rows",
const calledUrl = fetchMock.mock.calls[0][0] as string;
const url = new URL(calledUrl);
expect(url.searchParams.get("sort")).toBe(
- "-status,-new_fail_count,-changed_fail_count,-severity,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at",
+ "-status,-severity,-new_fail_count,-changed_fail_count,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at",
);
});
});
@@ -262,7 +262,7 @@ describe("getFindingGroupResources — Blocker 1: FAIL-first sort", () => {
const calledUrl = fetchMock.mock.calls[0][0] as string;
const url = new URL(calledUrl);
expect(url.searchParams.get("sort")).toBe(
- "-status,-delta,-severity,-last_seen_at",
+ "-status,-severity,-delta,-last_seen_at",
);
});
@@ -300,7 +300,7 @@ describe("getLatestFindingGroupResources — Blocker 1: FAIL-first sort", () =>
const calledUrl = fetchMock.mock.calls[0][0] as string;
const url = new URL(calledUrl);
expect(url.searchParams.get("sort")).toBe(
- "-status,-delta,-severity,-last_seen_at",
+ "-status,-severity,-delta,-last_seen_at",
);
});
@@ -344,7 +344,7 @@ describe("getFindingGroupResources — triangulation: params coexist", () => {
expect(url.searchParams.get("page[number]")).toBe("2");
expect(url.searchParams.get("page[size]")).toBe("50");
expect(url.searchParams.get("sort")).toBe(
- "-status,-delta,-severity,-last_seen_at",
+ "-status,-severity,-delta,-last_seen_at",
);
expect(url.searchParams.get("filter[status]")).toBeNull();
});
@@ -372,7 +372,7 @@ describe("getLatestFindingGroupResources — triangulation: params coexist", ()
expect(url.searchParams.get("page[number]")).toBe("3");
expect(url.searchParams.get("page[size]")).toBe("20");
expect(url.searchParams.get("sort")).toBe(
- "-status,-delta,-severity,-last_seen_at",
+ "-status,-severity,-delta,-last_seen_at",
);
expect(url.searchParams.get("filter[status]")).toBeNull();
});
@@ -443,7 +443,7 @@ describe("getFindingGroupResources — caller filters are preserved", () => {
const calledUrl = fetchMock.mock.calls[0][0] as string;
const url = new URL(calledUrl);
expect(url.searchParams.get("sort")).toBe(
- "-status,-delta,-severity,-last_seen_at",
+ "-status,-severity,-delta,-last_seen_at",
);
expect(url.searchParams.get("filter[name__icontains]")).toBe("bucket-prod");
expect(url.searchParams.get("filter[severity__in]")).toBe("high");
@@ -533,7 +533,7 @@ describe("getLatestFindingGroupResources — caller filters are preserved", () =
const calledUrl = fetchMock.mock.calls[0][0] as string;
const url = new URL(calledUrl);
expect(url.searchParams.get("sort")).toBe(
- "-status,-delta,-severity,-last_seen_at",
+ "-status,-severity,-delta,-last_seen_at",
);
expect(url.searchParams.get("filter[name__icontains]")).toBe(
"instance-prod",
diff --git a/ui/actions/finding-groups/finding-groups.ts b/ui/actions/finding-groups/finding-groups.ts
index c409d804b7..30798ea3e6 100644
--- a/ui/actions/finding-groups/finding-groups.ts
+++ b/ui/actions/finding-groups/finding-groups.ts
@@ -83,13 +83,13 @@ function normalizeFindingGroupResourceFilters(
}
const DEFAULT_FINDING_GROUPS_SORT =
- "-status,-new_fail_count,-changed_fail_count,-severity,-fail_count,-last_seen_at";
+ "-status,-severity,-new_fail_count,-changed_fail_count,-fail_count,-last_seen_at";
const DEFAULT_FINDING_GROUPS_SORT_WITH_MUTED =
- "-status,-new_fail_count,-changed_fail_count,-severity,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at";
+ "-status,-severity,-new_fail_count,-changed_fail_count,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at";
const DEFAULT_FINDING_GROUP_RESOURCES_SORT =
- "-status,-delta,-severity,-last_seen_at";
+ "-status,-severity,-delta,-last_seen_at";
interface FetchFindingGroupsParams {
page?: number;
diff --git a/ui/actions/findings/findings-by-resource.adapter.test.ts b/ui/actions/findings/findings-by-resource.adapter.test.ts
index b8781f0036..87d37c192c 100644
--- a/ui/actions/findings/findings-by-resource.adapter.test.ts
+++ b/ui/actions/findings/findings-by-resource.adapter.test.ts
@@ -115,4 +115,35 @@ describe("adaptFindingsByResourceResponse — malformed input", () => {
expect(result[0].id).toBe("finding-1");
expect(result[0].checkId).toBe("s3_check");
});
+
+ it("should normalize a single finding response into a one-item drawer array", () => {
+ // Given — getFindingById returns a single JSON:API resource object
+ const input = {
+ data: {
+ id: "finding-1",
+ attributes: {
+ uid: "uid-1",
+ check_id: "s3_check",
+ status: "FAIL",
+ severity: "critical",
+ check_metadata: {
+ checktitle: "S3 Check",
+ },
+ },
+ relationships: {
+ resources: { data: [] },
+ scan: { data: null },
+ },
+ },
+ included: [],
+ };
+
+ // When
+ const result = adaptFindingsByResourceResponse(input);
+
+ // Then
+ expect(result).toHaveLength(1);
+ expect(result[0].id).toBe("finding-1");
+ expect(result[0].checkTitle).toBe("S3 Check");
+ });
});
diff --git a/ui/actions/findings/findings-by-resource.adapter.ts b/ui/actions/findings/findings-by-resource.adapter.ts
index 75e8f5a81b..d1685eaa9f 100644
--- a/ui/actions/findings/findings-by-resource.adapter.ts
+++ b/ui/actions/findings/findings-by-resource.adapter.ts
@@ -165,16 +165,18 @@ type IncludedDict = Record;
* then resolves each finding's resource and provider relationships.
*/
interface JsonApiResponse {
- data: FindingApiItem[];
+ data: FindingApiItem | FindingApiItem[];
included?: Record[];
}
function isJsonApiResponse(value: unknown): value is JsonApiResponse {
+ const data = (value as { data?: unknown })?.data;
+
return (
value !== null &&
typeof value === "object" &&
"data" in value &&
- Array.isArray((value as { data: unknown }).data)
+ (Array.isArray(data) || (data !== null && typeof data === "object"))
);
}
@@ -188,8 +190,11 @@ export function adaptFindingsByResourceResponse(
const resourcesDict = createDict("resources", apiResponse) as IncludedDict;
const scansDict = createDict("scans", apiResponse) as IncludedDict;
const providersDict = createDict("providers", apiResponse) as IncludedDict;
+ const findings = Array.isArray(apiResponse.data)
+ ? apiResponse.data
+ : [apiResponse.data];
- return apiResponse.data.map((item) => {
+ return findings.map((item) => {
const attrs = item.attributes;
const meta = (attrs.check_metadata || {}) as Record;
const remediationRaw = meta.remediation as
diff --git a/ui/actions/findings/findings-by-resource.test.ts b/ui/actions/findings/findings-by-resource.test.ts
index 4aff44a2cf..7bc2793195 100644
--- a/ui/actions/findings/findings-by-resource.test.ts
+++ b/ui/actions/findings/findings-by-resource.test.ts
@@ -43,6 +43,7 @@ vi.mock("@/actions/finding-groups", () => ({
}));
import {
+ getLatestFindingsByResourceUid,
resolveFindingIdsByCheckIds,
resolveFindingIdsByVisibleGroupResources,
} from "./findings-by-resource";
@@ -262,3 +263,46 @@ describe("resolveFindingIdsByVisibleGroupResources", () => {
expect(fetchMock).not.toHaveBeenCalled();
});
});
+
+describe("getLatestFindingsByResourceUid", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("fetch", fetchMock);
+ getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
+ handleApiResponseMock.mockResolvedValue({ data: [] });
+ });
+
+ it("should restrict to FAIL, exclude muted findings, and apply severity/time sorting by default", async () => {
+ fetchMock.mockResolvedValue(new Response("", { status: 200 }));
+
+ await getLatestFindingsByResourceUid({
+ resourceUid: "resource-1",
+ });
+
+ const calledUrl = new URL(fetchMock.mock.calls[0][0]);
+ expect(calledUrl.pathname).toBe("/api/v1/findings/latest");
+ expect(calledUrl.searchParams.get("filter[resource_uid]")).toBe(
+ "resource-1",
+ );
+ // Status filter is applied server-side so the page[size]=50 window
+ // always holds FAIL rows — guards against PASS-heavy resources
+ // starving FAILs out of the result.
+ expect(calledUrl.searchParams.get("filter[status]")).toBe("FAIL");
+ expect(calledUrl.searchParams.get("filter[muted]")).toBe("false");
+ expect(calledUrl.searchParams.get("sort")).toBe("severity,-updated_at");
+ });
+
+ it("should include muted findings only when explicitly requested", async () => {
+ fetchMock.mockResolvedValue(new Response("", { status: 200 }));
+
+ await getLatestFindingsByResourceUid({
+ resourceUid: "resource-1",
+ includeMuted: true,
+ });
+
+ const calledUrl = new URL(fetchMock.mock.calls[0][0]);
+ expect(calledUrl.searchParams.get("filter[status]")).toBe("FAIL");
+ expect(calledUrl.searchParams.get("filter[muted]")).toBe("include");
+ expect(calledUrl.searchParams.get("sort")).toBe("severity,-updated_at");
+ });
+});
diff --git a/ui/actions/findings/findings-by-resource.ts b/ui/actions/findings/findings-by-resource.ts
index 344c5607a0..74a0bcb6de 100644
--- a/ui/actions/findings/findings-by-resource.ts
+++ b/ui/actions/findings/findings-by-resource.ts
@@ -250,10 +250,12 @@ export const getLatestFindingsByResourceUid = async ({
resourceUid,
page = 1,
pageSize = 50,
+ includeMuted = false,
}: {
resourceUid: string;
page?: number;
pageSize?: number;
+ includeMuted?: boolean;
}) => {
const headers = await getAuthHeaders({ contentType: false });
@@ -263,8 +265,8 @@ export const getLatestFindingsByResourceUid = async ({
url.searchParams.append("filter[resource_uid]", resourceUid);
url.searchParams.append("filter[status]", "FAIL");
- url.searchParams.append("filter[muted]", "include");
- url.searchParams.append("sort", "-severity,-updated_at");
+ url.searchParams.append("filter[muted]", includeMuted ? "include" : "false");
+ url.searchParams.append("sort", "severity,-updated_at");
if (page) url.searchParams.append("page[number]", page.toString());
if (pageSize) url.searchParams.append("page[size]", pageSize.toString());
diff --git a/ui/actions/findings/findings.ts b/ui/actions/findings/findings.ts
index 7ce7f931ad..242bd007a8 100644
--- a/ui/actions/findings/findings.ts
+++ b/ui/actions/findings/findings.ts
@@ -141,7 +141,15 @@ export const getLatestMetadataInfo = async ({
}
};
-export const getFindingById = async (findingId: string, include = "") => {
+interface GetFindingByIdOptions {
+ source?: "resource-detail-drawer";
+}
+
+export const getFindingById = async (
+ findingId: string,
+ include = "",
+ _options?: GetFindingByIdOptions,
+) => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/findings/${findingId}`);
diff --git a/ui/actions/providers/providers.test.ts b/ui/actions/providers/providers.test.ts
new file mode 100644
index 0000000000..ab3037db8b
--- /dev/null
+++ b/ui/actions/providers/providers.test.ts
@@ -0,0 +1,135 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const {
+ fetchMock,
+ getAuthHeadersMock,
+ getFormValueMock,
+ handleApiErrorMock,
+ handleApiResponseMock,
+} = vi.hoisted(() => ({
+ fetchMock: vi.fn(),
+ getAuthHeadersMock: vi.fn(),
+ getFormValueMock: vi.fn(),
+ handleApiErrorMock: vi.fn(),
+ handleApiResponseMock: vi.fn(),
+}));
+
+vi.mock("next/cache", () => ({
+ revalidatePath: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ redirect: vi.fn(),
+}));
+
+vi.mock("@/lib", () => ({
+ apiBaseUrl: "https://api.example.com/api/v1",
+ getAuthHeaders: getAuthHeadersMock,
+ getFormValue: getFormValueMock,
+ wait: vi.fn(),
+}));
+
+vi.mock("@/lib/provider-credentials/build-crendentials", () => ({
+ buildSecretConfig: vi.fn(() => ({
+ secretType: "access-secret-key",
+ secret: { key: "value" },
+ })),
+}));
+
+vi.mock("@/lib/provider-filters", () => ({
+ appendSanitizedProviderInFilters: vi.fn(),
+}));
+
+vi.mock("@/lib/server-actions-helper", () => ({
+ handleApiError: handleApiErrorMock,
+ handleApiResponse: handleApiResponseMock,
+}));
+
+import {
+ addCredentialsProvider,
+ addProvider,
+ checkConnectionProvider,
+ updateCredentialsProvider,
+} from "./providers";
+
+describe("providers actions", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("fetch", fetchMock);
+ getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
+ getFormValueMock.mockImplementation((formData: FormData, field: string) =>
+ formData.get(field),
+ );
+ handleApiErrorMock.mockReturnValue({ error: "Unexpected error" });
+ handleApiResponseMock.mockResolvedValue({ data: { id: "secret-1" } });
+ fetchMock.mockResolvedValue(
+ new Response(JSON.stringify({ data: { id: "secret-1" } }), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ }),
+ );
+ });
+
+ it("should revalidate providers after linking a cloud provider", async () => {
+ // Given
+ const formData = new FormData();
+ formData.set("providerType", "aws");
+ formData.set("providerUid", "111111111111");
+
+ // When
+ await addProvider(formData);
+
+ // Then
+ expect(handleApiResponseMock).toHaveBeenCalledWith(
+ expect.any(Response),
+ "/providers",
+ );
+ });
+
+ it("should revalidate providers after adding credentials in the wizard", async () => {
+ // Given
+ const formData = new FormData();
+ formData.set("providerId", "provider-1");
+ formData.set("providerType", "aws");
+
+ // When
+ await addCredentialsProvider(formData);
+
+ // Then
+ expect(handleApiResponseMock).toHaveBeenCalledWith(
+ expect.any(Response),
+ "/providers",
+ );
+ });
+
+ it("should revalidate providers after updating credentials in the wizard", async () => {
+ // Given
+ const formData = new FormData();
+ formData.set("providerId", "provider-1");
+ formData.set("providerType", "oraclecloud");
+
+ // When
+ await updateCredentialsProvider("secret-1", formData);
+
+ // Then
+ expect(handleApiResponseMock).toHaveBeenCalledWith(
+ expect.any(Response),
+ "/providers",
+ );
+ });
+
+ it("should revalidate providers when checking connection from the wizard", async () => {
+ // Given
+ const formData = new FormData();
+ formData.set("providerId", "provider-1");
+
+ // When
+ await checkConnectionProvider(formData);
+
+ // Then
+ expect(handleApiResponseMock).toHaveBeenCalledWith(
+ expect.any(Response),
+ "/providers",
+ );
+ });
+});
diff --git a/ui/actions/resources/index.ts b/ui/actions/resources/index.ts
index afaf91a381..600346e6c6 100644
--- a/ui/actions/resources/index.ts
+++ b/ui/actions/resources/index.ts
@@ -3,6 +3,7 @@ export {
getLatestResources,
getMetadataInfo,
getResourceById,
+ getResourceDrawerData,
getResourceEvents,
getResources,
} from "./resources";
diff --git a/ui/actions/resources/resources.ts b/ui/actions/resources/resources.ts
index 36f9761959..b0831bdb26 100644
--- a/ui/actions/resources/resources.ts
+++ b/ui/actions/resources/resources.ts
@@ -2,9 +2,12 @@
import { redirect } from "next/navigation";
+import { getLatestFindings } from "@/actions/findings";
+import { listOrganizationsSafe } from "@/actions/organizations/organizations";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
+import { OrganizationResource } from "@/types/organizations";
export const getResources = async ({
page = 1,
@@ -255,3 +258,57 @@ export const getResourceById = async (
return undefined;
}
};
+
+export const getResourceDrawerData = async ({
+ resourceId,
+ resourceUid,
+ providerId,
+ providerType,
+ page = 1,
+ pageSize = 10,
+ query = "",
+}: {
+ resourceId: string;
+ resourceUid: string;
+ providerId: string;
+ providerType: string;
+ page?: number;
+ pageSize?: number;
+ query?: string;
+}) => {
+ const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
+
+ const [resourceData, findingsResponse, organizationsResponse] =
+ await Promise.all([
+ getResourceById(resourceId, { fields: ["tags"] }),
+ getLatestFindings({
+ page,
+ pageSize,
+ query,
+ sort: "severity,-inserted_at",
+ filters: {
+ "filter[resource_uid]": resourceUid,
+ "filter[status]": "FAIL",
+ },
+ }),
+ isCloudEnv && providerType === "aws"
+ ? listOrganizationsSafe()
+ : Promise.resolve({ data: [] }),
+ ]);
+
+ const providerOrg =
+ providerType === "aws"
+ ? (organizationsResponse.data.find((organization: OrganizationResource) =>
+ organization.relationships?.providers?.data?.some(
+ (provider: { id: string }) => provider.id === providerId,
+ ),
+ ) ?? null)
+ : null;
+
+ return {
+ findings: findingsResponse?.data ?? [],
+ findingsMeta: findingsResponse?.meta ?? null,
+ providerOrg,
+ resourceTags: resourceData?.data?.attributes.tags ?? {},
+ };
+};
diff --git a/ui/app/(auth)/invitation/accept/accept-invitation-client.tsx b/ui/app/(auth)/invitation/accept/accept-invitation-client.tsx
index 73e6dbe8fd..f334053b48 100644
--- a/ui/app/(auth)/invitation/accept/accept-invitation-client.tsx
+++ b/ui/app/(auth)/invitation/accept/accept-invitation-client.tsx
@@ -8,10 +8,6 @@ import { useEffect, useRef, useState } from "react";
import { acceptInvitation } from "@/actions/invitations";
import { Button } from "@/components/shadcn";
-import {
- INVITATION_ACTION_PARAM,
- INVITATION_SIGNUP_ACTION,
-} from "@/lib/invitation-routing";
type AcceptState =
| { kind: "no-token" }
@@ -204,7 +200,7 @@ export function AcceptInvitationClient({
className="w-full"
onClick={() => {
router.push(
- `/sign-up?invitation_token=${encodeURIComponent(token!)}&${INVITATION_ACTION_PARAM}=${INVITATION_SIGNUP_ACTION}`,
+ `/sign-up?invitation_token=${encodeURIComponent(token!)}`,
);
}}
>
diff --git a/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.test.ts b/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.test.ts
new file mode 100644
index 0000000000..03b990c1d5
--- /dev/null
+++ b/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.test.ts
@@ -0,0 +1,16 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("findings view overview SSR", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "findings-view.ssr.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("uses the non-legacy latest findings columns", () => {
+ expect(source).toContain("ColumnLatestFindings");
+ expect(source).not.toContain("ColumnNewFindingsToDate");
+ });
+});
diff --git a/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx b/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx
index be2aec7770..cca971578c 100644
--- a/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx
+++ b/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx
@@ -3,7 +3,8 @@
import { getLatestFindings } from "@/actions/findings/findings";
import { LighthouseBanner } from "@/components/lighthouse/banner";
import { LinkToFindings } from "@/components/overview";
-import { ColumnNewFindingsToDate } from "@/components/overview/new-findings-table/table/column-new-findings-to-date";
+import { ColumnLatestFindings } from "@/components/overview/new-findings-table/table";
+import { CardTitle } from "@/components/shadcn";
import { DataTable } from "@/components/ui/table";
import { createDict } from "@/lib/helper";
import { FindingProps, SearchParamsProps } from "@/types";
@@ -57,24 +58,23 @@ export async function FindingsViewSSR({ searchParams }: FindingsViewSSRProps) {
};
return (
-
+
-
-
-
- Latest new failing findings
-
-
- Showing the latest 10 new failing findings by severity.
-
-
-
-
-
+
+
Latest New Failed Findings
+
+ Showing the latest 10 sorted by severity
+
+
+
+
+ }
/>
);
diff --git a/ui/app/(prowler)/_overview/graphs-tabs/graphs-tabs-wrapper.tsx b/ui/app/(prowler)/_overview/graphs-tabs/graphs-tabs-wrapper.tsx
index c21491e37f..f9741dc75e 100644
--- a/ui/app/(prowler)/_overview/graphs-tabs/graphs-tabs-wrapper.tsx
+++ b/ui/app/(prowler)/_overview/graphs-tabs/graphs-tabs-wrapper.tsx
@@ -1,6 +1,7 @@
import { Skeleton } from "@heroui/skeleton";
import { Suspense } from "react";
+import { SkeletonTableNewFindings } from "@/components/overview/new-findings-table/table";
import { SearchParamsProps } from "@/types";
import { GraphsTabsClient } from "./_components/graphs-tabs-client";
@@ -18,6 +19,10 @@ const LoadingFallback = () => (
);
+const TAB_FALLBACKS: Partial> = {
+ findings: ,
+};
+
type GraphComponent = React.ComponentType<{ searchParams: SearchParamsProps }>;
const GRAPH_COMPONENTS: Record = {
@@ -38,9 +43,10 @@ export const GraphsTabsWrapper = async ({
const tabsContent = Object.fromEntries(
GRAPH_TABS.map((tab) => {
const Component = GRAPH_COMPONENTS[tab.id];
+ const fallback = TAB_FALLBACKS[tab.id] ?? ;
return [
tab.id,
- }>
+
,
];
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.test.tsx
new file mode 100644
index 0000000000..d7209c68c8
--- /dev/null
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.test.tsx
@@ -0,0 +1,93 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import type { ReactNode } from "react";
+import { describe, expect, it, vi } from "vitest";
+
+import type { GraphNode } from "@/types/attack-paths";
+
+import { NodeDetailPanel } from "./node-detail-panel";
+
+vi.mock("@/components/ui/sheet/sheet", () => ({
+ Sheet: ({ children }: { children: ReactNode }) => {children}
,
+ SheetContent: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ SheetDescription: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ SheetHeader: ({ children }: { children: ReactNode }) => {children}
,
+ SheetTitle: ({ children }: { children: ReactNode }) => {children}
,
+}));
+
+vi.mock("./node-overview", () => ({
+ NodeOverview: () => Node overview
,
+}));
+
+vi.mock("./node-findings", () => ({
+ NodeFindings: () => Node findings
,
+}));
+
+vi.mock("./node-resources", () => ({
+ NodeResources: () => Node resources
,
+}));
+
+const findingNode: GraphNode = {
+ id: "graph-node-id",
+ labels: ["ProwlerFinding"],
+ properties: {
+ id: "finding-123",
+ check_title: "Open S3 bucket",
+ name: "Open S3 bucket",
+ },
+};
+
+const resourceNode: GraphNode = {
+ id: "resource-node-id",
+ labels: ["S3Bucket"],
+ properties: {
+ id: "bucket-123",
+ name: "bucket-123",
+ },
+};
+
+describe("NodeDetailPanel", () => {
+ it("renders the view finding button only for finding nodes", () => {
+ const { rerender } = render( );
+
+ expect(
+ screen.getByRole("button", { name: /view finding finding-123/i }),
+ ).toBeInTheDocument();
+
+ rerender( );
+
+ expect(
+ screen.queryByRole("button", { name: /view finding/i }),
+ ).not.toBeInTheDocument();
+ });
+
+ it("calls onViewFinding with the node finding id", async () => {
+ const user = userEvent.setup();
+ const onViewFinding = vi.fn();
+
+ render(
+ ,
+ );
+
+ await user.click(
+ screen.getByRole("button", { name: /view finding finding-123/i }),
+ );
+
+ expect(onViewFinding).toHaveBeenCalledWith("finding-123");
+ });
+
+ it("disables the button and shows the spinner while loading", () => {
+ render( );
+
+ const button = screen.getByRole("button", {
+ name: /view finding finding-123/i,
+ });
+
+ expect(button).toBeDisabled();
+ expect(screen.getByLabelText("Loading")).toHaveClass("size-4");
+ });
+});
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.tsx
index 4d8885e65c..97d8657bfd 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.tsx
@@ -1,6 +1,7 @@
"use client";
import { Button, Card, CardContent } from "@/components/shadcn";
+import { Spinner } from "@/components/shadcn/spinner/spinner";
import {
Sheet,
SheetContent,
@@ -18,6 +19,8 @@ interface NodeDetailPanelProps {
node: GraphNode | null;
allNodes?: GraphNode[];
onClose?: () => void;
+ onViewFinding?: (findingId: string) => void;
+ viewFindingLoading?: boolean;
}
/**
@@ -26,9 +29,13 @@ interface NodeDetailPanelProps {
export const NodeDetailContent = ({
node,
allNodes = [],
+ onViewFinding,
+ viewFindingLoading = false,
}: {
node: GraphNode;
allNodes?: GraphNode[];
+ onViewFinding?: (findingId: string) => void;
+ viewFindingLoading?: boolean;
}) => {
const isProwlerFinding = node?.labels.some((label) =>
label.toLowerCase().includes("finding"),
@@ -56,7 +63,12 @@ export const NodeDetailContent = ({
Findings connected to this node
-
+
)}
@@ -88,12 +100,15 @@ export const NodeDetailPanel = ({
node,
allNodes = [],
onClose,
+ onViewFinding,
+ viewFindingLoading = false,
}: NodeDetailPanelProps) => {
const isOpen = node !== null;
const isProwlerFinding = node?.labels.some((label) =>
label.toLowerCase().includes("finding"),
);
+ const findingId = node ? String(node.properties?.id || node.id) : "";
return (
!open && onClose?.()}>
@@ -107,15 +122,19 @@ export const NodeDetailPanel = ({
{node && isProwlerFinding && (
-
-
- View Finding →
-
+ onViewFinding?.(findingId)}
+ disabled={viewFindingLoading}
+ aria-label={`View finding ${findingId}`}
+ >
+ {viewFindingLoading ? (
+
+ ) : (
+ "View Finding →"
+ )}
)}
@@ -123,7 +142,12 @@ export const NodeDetailPanel = ({
{node && (
-
+
)}
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-findings.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-findings.tsx
index bb424a818c..253401fa97 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-findings.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-findings.tsx
@@ -1,5 +1,7 @@
"use client";
+import { Button } from "@/components/shadcn";
+import { Spinner } from "@/components/shadcn/spinner/spinner";
import { SeverityBadge } from "@/components/ui/table/severity-badge";
import type { GraphNode } from "@/types/attack-paths";
@@ -16,13 +18,20 @@ type Severity = (typeof SEVERITY_LEVELS)[keyof typeof SEVERITY_LEVELS];
interface NodeFindingsProps {
node: GraphNode;
allNodes?: GraphNode[];
+ onViewFinding?: (findingId: string) => void;
+ viewFindingLoading?: boolean;
}
/**
* Node findings section showing related findings for the selected node
* Displays findings that are connected to the node via HAS_FINDING edges
*/
-export const NodeFindings = ({ node, allNodes = [] }: NodeFindingsProps) => {
+export const NodeFindings = ({
+ node,
+ allNodes = [],
+ onViewFinding,
+ viewFindingLoading = false,
+}: NodeFindingsProps) => {
// Get finding IDs from the node's findings array (populated by adapter)
const findingIds = node.findings || [];
@@ -79,15 +88,20 @@ export const NodeFindings = ({ node, allNodes = [] }: NodeFindingsProps) => {
ID: {findingId}
- onViewFinding?.(findingId)}
+ disabled={viewFindingLoading}
aria-label={`View full finding for ${findingName}`}
className="text-text-info dark:text-text-info h-auto shrink-0 p-0 text-xs font-medium hover:underline"
>
- View Full Finding →
-
+ {viewFindingLoading ? (
+
+ ) : (
+ "View Full Finding →"
+ )}
+
{finding.properties?.description && (
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-remediation.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-remediation.tsx
index e3421e5a43..3ece3184f0 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-remediation.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-remediation.tsx
@@ -1,8 +1,8 @@
"use client";
-import Link from "next/link";
-
import { Badge } from "@/components/shadcn/badge/badge";
+import { Button } from "@/components/shadcn/button/button";
+import { Spinner } from "@/components/shadcn/spinner/spinner";
interface Finding {
id: string;
@@ -13,12 +13,18 @@ interface Finding {
interface NodeRemediationProps {
findings: Finding[];
+ onViewFinding?: (findingId: string) => void;
+ viewFindingLoading?: boolean;
}
/**
* Node remediation section showing related Prowler findings
*/
-export const NodeRemediation = ({ findings }: NodeRemediationProps) => {
+export const NodeRemediation = ({
+ findings,
+ onViewFinding,
+ viewFindingLoading = false,
+}: NodeRemediationProps) => {
const getSeverityVariant = (severity: string) => {
switch (severity) {
case "critical":
@@ -66,15 +72,20 @@ export const NodeRemediation = ({ findings }: NodeRemediationProps) => {
- onViewFinding?.(finding.id)}
+ disabled={viewFindingLoading}
aria-label={`View full finding for ${finding.title}`}
- className="text-text-info dark:text-text-info text-sm transition-all hover:opacity-80 dark:hover:opacity-80"
+ className="text-text-info dark:text-text-info h-auto p-0 text-sm transition-all hover:opacity-80 dark:hover:opacity-80"
>
- View Full Finding →
-
+ {viewFindingLoading ? (
+
+ ) : (
+ "View Full Finding →"
+ )}
+
))}
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.test.tsx
index a029481e7a..4aec4b97d3 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.test.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.test.tsx
@@ -24,6 +24,19 @@ vi.mock("next/navigation", () => ({
useSearchParams: () => navigationState.searchParams,
}));
+vi.mock("@/components/shadcn/tooltip", () => ({
+ Tooltip: ({ children }: { children: ReactNode }) => <>{children}>,
+ TooltipTrigger: ({
+ children,
+ }: {
+ children: ReactNode;
+ asChild?: boolean;
+ }) => <>{children}>,
+ TooltipContent: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+}));
+
vi.mock("@/components/ui/entities/entity-info", () => ({
EntityInfo: ({
entityAlias,
@@ -156,14 +169,14 @@ describe("ScanListTable", () => {
expect(screen.getByText("12 Total Entries")).toBeInTheDocument();
expect(screen.getByText("Page 1 of 3")).toBeInTheDocument();
- await user.click(screen.getAllByRole("button", { name: "Select scan" })[0]);
+ await user.click(screen.getAllByRole("radio", { name: "Select scan" })[0]);
expect(pushMock).toHaveBeenCalledWith(
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
);
});
- it("enables the select button for a failed scan when graph data is ready", async () => {
+ it("enables the radio button for a failed scan when graph data is ready", async () => {
const user = userEvent.setup();
const failedScan: AttackPathScan = {
...createScan(1),
@@ -176,18 +189,18 @@ describe("ScanListTable", () => {
render( );
- const button = screen.getByRole("button", { name: "Select scan" });
- expect(button).toBeEnabled();
- expect(button).toHaveTextContent("Select");
+ const radio = screen.getByRole("radio", { name: "Select scan" });
+ expect(radio).toBeEnabled();
+ expect(radio).toHaveAttribute("aria-checked", "false");
- await user.click(button);
+ await user.click(radio);
expect(pushMock).toHaveBeenCalledWith(
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
);
});
- it("disables the select button for a failed scan when graph data is not ready", () => {
+ it("disables the radio button for a failed scan when graph data is not ready", () => {
const failedScan: AttackPathScan = {
...createScan(1),
attributes: {
@@ -199,8 +212,114 @@ describe("ScanListTable", () => {
render( );
- const button = screen.getByRole("button", { name: "Select scan" });
- expect(button).toBeDisabled();
- expect(button).toHaveTextContent("Failed");
+ const radio = screen.getByRole("radio", { name: "Scan not available" });
+ expect(radio).toBeDisabled();
+ });
+
+ it("shows a disabled radio button for a scheduled scan without graph data", () => {
+ const scheduledScan: AttackPathScan = {
+ ...createScan(1),
+ attributes: {
+ ...createScan(1).attributes,
+ state: "scheduled",
+ progress: 0,
+ graph_data_ready: false,
+ completed_at: null,
+ duration: null,
+ },
+ };
+
+ render( );
+
+ const radio = screen.getByRole("radio", { name: "Scan not available" });
+ expect(radio).toBeDisabled();
+ });
+
+ it("shows a disabled radio button for an executing scan without graph data", () => {
+ const executingScan: AttackPathScan = {
+ ...createScan(1),
+ attributes: {
+ ...createScan(1).attributes,
+ state: "executing",
+ progress: 45,
+ graph_data_ready: false,
+ completed_at: null,
+ duration: null,
+ },
+ };
+
+ render( );
+
+ const radio = screen.getByRole("radio", { name: "Scan not available" });
+ expect(radio).toBeDisabled();
+ });
+
+ it("enables the radio button for a scheduled scan when graph data is ready from a previous cycle", async () => {
+ const user = userEvent.setup();
+ const scheduledWithGraph: AttackPathScan = {
+ ...createScan(1),
+ attributes: {
+ ...createScan(1).attributes,
+ state: "scheduled",
+ progress: 0,
+ graph_data_ready: true,
+ },
+ };
+
+ render( );
+
+ const radio = screen.getByRole("radio", { name: "Select scan" });
+ expect(radio).toBeEnabled();
+ expect(radio).toHaveAttribute("aria-checked", "false");
+
+ await user.click(radio);
+
+ expect(pushMock).toHaveBeenCalledWith(
+ "/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
+ );
+ });
+
+ it("exposes an accessible label in the Graph column when graph data is ready", () => {
+ render( );
+
+ expect(screen.getByLabelText("Graph available")).toHaveClass(
+ "text-text-success-primary",
+ );
+ });
+
+ it("exposes an accessible label in the Graph column when graph data is not ready", () => {
+ const noGraphScan: AttackPathScan = {
+ ...createScan(1),
+ attributes: {
+ ...createScan(1).attributes,
+ graph_data_ready: false,
+ },
+ };
+
+ render( );
+
+ expect(screen.getByLabelText("Graph not available")).toHaveClass(
+ "text-text-neutral-secondary",
+ );
+ });
+
+ it("renders a tooltip explaining a completed scan without graph data", () => {
+ const completedNoGraph: AttackPathScan = {
+ ...createScan(1),
+ attributes: {
+ ...createScan(1).attributes,
+ state: "completed",
+ graph_data_ready: false,
+ },
+ };
+
+ render( );
+
+ expect(
+ screen.getByRole("radio", { name: "Scan not available" }),
+ ).toBeDisabled();
+ expect(
+ screen.getByText("This scan completed without producing graph data."),
+ ).toBeInTheDocument();
});
});
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx
index b5bd240cf0..aa686be15c 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx
@@ -1,15 +1,25 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
+import { Check, Minus } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
-import { Button } from "@/components/shadcn/button/button";
+import {
+ RadioGroup,
+ RadioGroupItem,
+} from "@/components/shadcn/radio-group/radio-group";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/shadcn/tooltip";
import { DateWithTime } from "@/components/ui/entities/date-with-time";
import { EntityInfo } from "@/components/ui/entities/entity-info";
import { DataTable, DataTableColumnHeader } from "@/components/ui/table";
import { formatDuration } from "@/lib/date-utils";
+import { cn } from "@/lib/utils";
import type { MetaDataProps, ProviderType } from "@/types";
-import type { AttackPathScan, ScanState } from "@/types/attack-paths";
+import type { AttackPathScan } from "@/types/attack-paths";
import { SCAN_STATES } from "@/types/attack-paths";
import { ScanStatusBadge } from "./scan-status-badge";
@@ -20,12 +30,6 @@ interface ScanListTableProps {
const DEFAULT_PAGE_SIZE = 5;
const PAGE_SIZE_OPTIONS = [2, 5, 10, 15];
-const WAITING_STATES: readonly ScanState[] = [
- SCAN_STATES.SCHEDULED,
- SCAN_STATES.AVAILABLE,
- SCAN_STATES.EXECUTING,
-];
-
const parsePageParam = (value: string | null, fallback: number) => {
if (!value) return fallback;
@@ -38,34 +42,32 @@ const formatNullableDuration = (duration: number | null) => {
return formatDuration(duration);
};
-const isSelectDisabled = (
- scan: AttackPathScan,
- selectedScanId: string | null,
-) => {
- return !scan.attributes.graph_data_ready || selectedScanId === scan.id;
-};
-
-const getSelectButtonLabel = (
- scan: AttackPathScan,
- selectedScanId: string | null,
-) => {
- if (selectedScanId === scan.id) {
- return "Selected";
- }
-
+const getDisabledTooltip = (scan: AttackPathScan): string | null => {
if (scan.attributes.graph_data_ready) {
- return "Select";
+ return null;
}
- if (WAITING_STATES.includes(scan.attributes.state)) {
- return "Waiting...";
+ if (scan.attributes.state === SCAN_STATES.SCHEDULED) {
+ return "Graph will be available once this scan runs and completes.";
+ }
+
+ if (scan.attributes.state === SCAN_STATES.AVAILABLE) {
+ return "This scan is queued. Graph will be available once it completes.";
+ }
+
+ if (scan.attributes.state === SCAN_STATES.EXECUTING) {
+ return "Scan is running. Graph will be available once it completes.";
}
if (scan.attributes.state === SCAN_STATES.FAILED) {
- return "Failed";
+ return "This scan failed. No graph data is available.";
}
- return "Select";
+ if (scan.attributes.state === SCAN_STATES.COMPLETED) {
+ return "This scan completed without producing graph data.";
+ }
+
+ return "Graph data is not available for this scan.";
};
const getSelectedRowSelection = (
@@ -97,11 +99,54 @@ const buildMetadata = (
const getColumns = ({
selectedScanId,
- onSelectScan,
}: {
selectedScanId: string | null;
- onSelectScan: (scanId: string) => void;
}): ColumnDef[] => [
+ {
+ id: "select",
+ header: () => Select ,
+ cell: ({ row }) => {
+ const isSelected = selectedScanId === row.original.id;
+ const canSelect = row.original.attributes.graph_data_ready;
+ const tooltip = getDisabledTooltip(row.original);
+
+ const radio = (
+
+ );
+
+ if (!canSelect && !isSelected && tooltip) {
+ return (
+
+
+ {radio}
+
+ {tooltip}
+
+ );
+ }
+
+ return radio;
+ },
+ enableSorting: false,
+ },
{
accessorKey: "provider",
header: ({ column }) => (
@@ -135,22 +180,32 @@ const getColumns = ({
),
cell: ({ row }) => (
-
+
+
+
),
enableSorting: false,
},
{
- accessorKey: "progress",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => (
- {row.original.attributes.progress}%
- ),
+ accessorKey: "graph_data_ready",
+ header: () => Graph ,
+ cell: ({ row }) =>
+ row.original.attributes.graph_data_ready ? (
+
+ ) : (
+
+ ),
enableSorting: false,
},
{
@@ -165,29 +220,6 @@ const getColumns = ({
),
enableSorting: false,
},
- {
- id: "actions",
- header: () => Actions ,
- cell: ({ row }) => {
- const isDisabled = isSelectDisabled(row.original, selectedScanId);
-
- return (
-
- onSelectScan(row.original.id)}
- className="w-full max-w-24"
- >
- {getSelectButtonLabel(row.original, selectedScanId)}
-
-
- );
- },
- enableSorting: false,
- },
];
/**
@@ -237,19 +269,27 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
};
return (
-
+
+ {
+ if (row.original.attributes.graph_data_ready) {
+ handleSelectScan(row.original.id);
+ }
+ }}
+ enableRowSelection
+ rowSelection={getSelectedRowSelection(paginatedScans, selectedScanId)}
+ />
+
);
};
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-status-badge.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-status-badge.tsx
index de8833749e..0c04f95257 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-status-badge.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-status-badge.tsx
@@ -3,94 +3,55 @@
import { Loader2 } from "lucide-react";
import { Badge } from "@/components/shadcn/badge/badge";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/shadcn/tooltip";
+import { cn } from "@/lib/utils";
import type { ScanState } from "@/types/attack-paths";
import { SCAN_STATES } from "@/types/attack-paths";
-const BADGE_CONFIG: Record<
- ScanState,
- { className: string; label: string; showGraphDot: boolean }
-> = {
+const BADGE_CONFIG: Record = {
[SCAN_STATES.SCHEDULED]: {
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
label: "Scheduled",
- showGraphDot: true,
},
[SCAN_STATES.AVAILABLE]: {
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
label: "Queued",
- showGraphDot: true,
},
[SCAN_STATES.EXECUTING]: {
- className: "bg-bg-warning-secondary text-text-neutral-primary",
+ className: "bg-bg-info-secondary text-text-info",
label: "In Progress",
- showGraphDot: false,
},
[SCAN_STATES.COMPLETED]: {
className: "bg-bg-pass-secondary text-text-success-primary",
label: "Completed",
- showGraphDot: false,
},
[SCAN_STATES.FAILED]: {
className: "bg-bg-fail-secondary text-text-error-primary",
label: "Failed",
- showGraphDot: true,
},
};
interface ScanStatusBadgeProps {
status: ScanState;
progress?: number;
- graphDataReady?: boolean;
}
export const ScanStatusBadge = ({
status,
progress = 0,
- graphDataReady = false,
}: ScanStatusBadgeProps) => {
const config = BADGE_CONFIG[status];
- const graphDot = graphDataReady && config.showGraphDot && (
-
- );
-
- const tooltipText = graphDataReady
- ? "Graph available"
- : status === SCAN_STATES.FAILED || status === SCAN_STATES.COMPLETED
- ? "Graph not available"
- : "Graph not available yet";
-
- const icon =
- status === SCAN_STATES.EXECUTING ? (
-
- ) : (
- graphDot
- );
-
const label =
status === SCAN_STATES.EXECUTING
? `${config.label} (${progress}%)`
: config.label;
return (
-
-
-
- {icon}
- {label}
-
-
- {tooltipText}
-
+
+ {status === SCAN_STATES.EXECUTING && (
+
+ )}
+ {label}
+
);
};
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx
index f6b1007f5a..4fbd48c793 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx
@@ -14,6 +14,8 @@ import {
getAvailableQueries,
} from "@/actions/attack-paths";
import { adaptQueryResultToGraphData } from "@/actions/attack-paths/query-result.adapter";
+import { FindingDetailDrawer } from "@/components/findings/table";
+import { useFindingDetails } from "@/components/resources/table/use-finding-details";
import { AutoRefresh } from "@/components/scans";
import {
Alert,
@@ -30,6 +32,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/shadcn/dialog";
+import { Spinner } from "@/components/shadcn/spinner/spinner";
import { useToast } from "@/components/ui";
import type {
AttackPathQuery,
@@ -37,7 +40,7 @@ import type {
AttackPathScan,
GraphNode,
} from "@/types/attack-paths";
-import { ATTACK_PATH_QUERY_IDS } from "@/types/attack-paths";
+import { ATTACK_PATH_QUERY_IDS, SCAN_STATES } from "@/types/attack-paths";
import {
AttackPathGraph,
@@ -65,6 +68,7 @@ export default function AttackPathsPage() {
const searchParams = useSearchParams();
const scanId = searchParams.get("scanId");
const graphState = useGraphState();
+ const finding = useFindingDetails();
const { toast } = useToast();
const [scansLoading, setScansLoading] = useState(true);
@@ -116,10 +120,17 @@ export default function AttackPathsPage() {
// Check if there's an executing scan for auto-refresh
const hasExecutingScan = scans.some(
(scan) =>
- scan.attributes.state === "executing" ||
- scan.attributes.state === "scheduled",
+ scan.attributes.state === SCAN_STATES.EXECUTING ||
+ scan.attributes.state === SCAN_STATES.SCHEDULED,
);
+ // Detect if the selected scan is showing data from a previous cycle
+ const selectedScan = scans.find((scan) => scan.id === scanId);
+ const isViewingPreviousCycleData =
+ selectedScan &&
+ selectedScan.attributes.graph_data_ready &&
+ selectedScan.attributes.state !== SCAN_STATES.COMPLETED;
+
// Callback to refresh scans (used by AutoRefresh component)
const refreshScans = async () => {
try {
@@ -304,6 +315,14 @@ export default function AttackPathsPage() {
graphState.selectNode(null);
};
+ const getFindingId = (node: GraphNode | null) =>
+ node ? String(node.properties?.id || node.id) : "";
+
+ const handleViewFinding = (findingId: string) => {
+ if (!findingId) return;
+ void finding.navigateToFinding(findingId);
+ };
+
const handleGraphExport = (svgElement: SVGSVGElement | null) => {
try {
if (svgElement) {
@@ -339,11 +358,11 @@ export default function AttackPathsPage() {
Attack Paths
-
+
Select a scan, build a query, and visualize Attack Paths in your
infrastructure.
-
+
Scans can be selected when data is available. A new scan does not
interrupt access to existing data.
@@ -373,6 +392,21 @@ export default function AttackPathsPage() {
+ {/* Banner: viewing data from a previous scan cycle */}
+ {isViewingPreviousCycleData && (
+
+
+ Viewing data from a previous scan
+
+ This scan is currently{" "}
+ {selectedScan.attributes.state === SCAN_STATES.EXECUTING
+ ? `running (${selectedScan.attributes.progress}%)`
+ : selectedScan.attributes.state}
+ . The graph data shown is from the last completed cycle.
+
+
+ )}
+
{/* Query Builder Section - shown only after selecting a scan */}
{scanId && (
@@ -568,7 +602,7 @@ export default function AttackPathsPage() {
-
+
{graphState.selectedNode?.labels.some(
(label) =>
label
@@ -591,7 +625,7 @@ export default function AttackPathsPage() {
Type
-
+
{graphState.selectedNode?.labels
.map(formatNodeLabel)
.join(", ")}
@@ -641,7 +675,7 @@ export default function AttackPathsPage() {
Node Details
-
+
{String(
graphState.selectedNode.labels.some((label) =>
label.toLowerCase().includes("finding"),
@@ -659,15 +693,20 @@ export default function AttackPathsPage() {
{graphState.selectedNode.labels.some((label) =>
label.toLowerCase().includes("finding"),
) && (
-
-
- View Finding →
-
+
+ handleViewFinding(getFindingId(graphState.selectedNode))
+ }
+ disabled={finding.findingDetailLoading}
+ aria-label={`View finding ${getFindingId(graphState.selectedNode)}`}
+ >
+ {finding.findingDetailLoading ? (
+
+ ) : (
+ "View Finding"
+ )}
)}
)}
+
+ {finding.findingDetails && (
+
{
+ if (!open) finding.resetFindingDetails();
+ }}
+ />
+ )}
>
)}
diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx
index dd86be1f8a..069826c3ae 100644
--- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx
+++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx
@@ -78,7 +78,7 @@ export default async function ComplianceDetail({
await Promise.all([
getComplianceOverviewMetadataInfo({
filters: {
- "filter[scan_id]": selectedScanId,
+ "filter[scan_id]": selectedScanId ?? undefined,
},
}),
getComplianceAttributes(complianceId),
diff --git a/ui/app/(prowler)/compliance/page.test.tsx b/ui/app/(prowler)/compliance/page.test.tsx
new file mode 100644
index 0000000000..42bbbe672f
--- /dev/null
+++ b/ui/app/(prowler)/compliance/page.test.tsx
@@ -0,0 +1,16 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("Compliance overview page", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "page.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("delegates client-side search to ComplianceOverviewGrid", () => {
+ expect(source).toContain("ComplianceOverviewGrid");
+ expect(source).not.toContain("filter[search]");
+ });
+});
diff --git a/ui/app/(prowler)/compliance/page.tsx b/ui/app/(prowler)/compliance/page.tsx
index e3e0a63705..92015cd142 100644
--- a/ui/app/(prowler)/compliance/page.tsx
+++ b/ui/app/(prowler)/compliance/page.tsx
@@ -1,3 +1,4 @@
+import { Info } from "lucide-react";
import { Suspense } from "react";
import {
@@ -7,12 +8,14 @@ import {
import { getThreatScore } from "@/actions/overview";
import { getScans } from "@/actions/scans";
import {
- ComplianceCard,
ComplianceSkeletonGrid,
NoScansAvailable,
ThreatScoreBadge,
} from "@/components/compliance";
-import { ComplianceHeader } from "@/components/compliance/compliance-header/compliance-header";
+import { ComplianceFilters } from "@/components/compliance/compliance-header/compliance-filters";
+import { ComplianceOverviewGrid } from "@/components/compliance/compliance-overview-grid";
+import { Alert, AlertDescription } from "@/components/shadcn/alert";
+import { Card, CardContent } from "@/components/shadcn/card/card";
import { ContentLayout } from "@/components/ui";
import {
ExpandedScanData,
@@ -30,12 +33,6 @@ export default async function Compliance({
const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
- const filters = Object.fromEntries(
- Object.entries(resolvedSearchParams).filter(([key]) =>
- key.startsWith("filter["),
- ),
- );
-
const scansData = await getScans({
filters: {
"filter[state]": "completed",
@@ -79,9 +76,12 @@ export default async function Compliance({
.filter(Boolean) as ExpandedScanData[];
// Use scanId from URL, or select the first scan if not provided
- const selectedScanId =
- resolvedSearchParams.scanId || expandedScansData[0]?.id || null;
- const query = (filters["filter[search]"] as string) || "";
+ const scanIdParam = resolvedSearchParams.scanId;
+ const scanIdFromUrl = Array.isArray(scanIdParam)
+ ? scanIdParam[0]
+ : scanIdParam;
+ const selectedScanId: string | null =
+ scanIdFromUrl || expandedScansData[0]?.id || null;
// Find the selected scan
const selectedScan = expandedScansData.find(
@@ -102,7 +102,6 @@ export default async function Compliance({
// Fetch metadata if we have a selected scan
const metadataInfoData = selectedScanId
? await getComplianceOverviewMetadataInfo({
- query,
filters: {
"filter[scan_id]": selectedScanId,
},
@@ -131,28 +130,39 @@ export default async function Compliance({
{selectedScanId ? (
<>
-
-
-
-
- {threatScoreData &&
- typeof selectedScanId === "string" &&
- selectedScan && (
-
-
-
- )}
+ {/* Row 1: Filters */}
+
+
-
}>
+
+ {/* Row 2: ThreatScore card — full width, horizontal */}
+ {threatScoreData &&
+ typeof selectedScanId === "string" &&
+ selectedScan && (
+
+
+
+ )}
+
+ {/* Row 3: Compliance grid with client-side search */}
+
+
+
+ }
+ >
key.startsWith("filter[")),
- );
-
- // Extract query from filters
- const query = (filters["filter[search]"] as string) || "";
-
// Only fetch compliance data if we have a valid scanId
const compliancesData =
scanId && scanId.trim() !== ""
? await getCompliancesOverview({
scanId,
region: regionFilter,
- query,
})
: { data: [], errors: [] };
const type = compliancesData?.data?.type;
+ const frameworks = compliancesData?.data
+ ?.filter((compliance: ComplianceOverviewData) => {
+ return compliance.attributes.framework !== "ProwlerThreatScore";
+ })
+ .sort((a: ComplianceOverviewData, b: ComplianceOverviewData) =>
+ a.attributes.framework.localeCompare(b.attributes.framework),
+ );
// Check if the response contains no data
if (
@@ -204,58 +212,49 @@ const SSRComplianceGrid = async ({
type === "tasks"
) {
return (
-
-
- No compliance data available for the selected scan.
-
-
+
+
+
+ This scan has no compliance data available yet, please select a
+ different one.
+
+
);
}
// Handle errors returned by the API
if (compliancesData?.errors?.length > 0) {
return (
-
-
Provide a valid scan ID.
-
+
+
+ Provide a valid scan ID.
+
);
}
return (
-
- {compliancesData.data
- .filter((compliance: ComplianceOverviewData) => {
- // Filter out ProwlerThreatScore from the grid
- return compliance.attributes.framework !== "ProwlerThreatScore";
- })
- .sort((a: ComplianceOverviewData, b: ComplianceOverviewData) =>
- a.attributes.framework.localeCompare(b.attributes.framework),
- )
- .map((compliance: ComplianceOverviewData) => {
- const { attributes, id } = compliance;
- const {
- framework,
- version,
- requirements_passed,
- total_requirements,
- } = attributes;
-
- return (
-
- );
- })}
-
+
+
+
+ );
+};
+
+const ComplianceOverviewPanel = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ return (
+
+ {children}
+
);
};
diff --git a/ui/app/(prowler)/findings/page.test.ts b/ui/app/(prowler)/findings/page.test.ts
index 76462dff99..444ed47f9e 100644
--- a/ui/app/(prowler)/findings/page.test.ts
+++ b/ui/app/(prowler)/findings/page.test.ts
@@ -25,8 +25,10 @@ describe("findings page", () => {
expect(source).toContain("resolveFindingScanDateFilters");
});
- it("uses getLatestFindingGroups for non-date/scan queries and getFindingGroups for historical", () => {
- expect(source).toContain("hasDateOrScan");
+ it("uses resolved filters to choose getFindingGroups for historical queries and getLatestFindingGroups otherwise", () => {
+ expect(source).toContain("hasHistoricalData");
+ expect(source).toContain("hasDateOrScanFilter(filtersWithScanDates)");
+ expect(source).toContain("hasDateOrScanFilter(filters)");
expect(source).toContain("getFindingGroups");
expect(source).toContain("getLatestFindingGroups");
});
diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx
index 2211800451..22c90330bb 100644
--- a/ui/app/(prowler)/findings/page.tsx
+++ b/ui/app/(prowler)/findings/page.tsx
@@ -34,12 +34,6 @@ export default async function Findings({
const { encodedSort } = extractSortAndKey(resolvedSearchParams);
const { filters, query } = extractFiltersAndQuery(resolvedSearchParams);
- // Check if the searchParams contain any date or scan filter
- const hasDateOrScan = hasDateOrScanFilter(resolvedSearchParams);
-
- // TODO: Re-implement deep link support (/findings?id=) using the grouped view's resource detail drawer
- // once the legacy FindingDetailsSheet is fully deprecated (still used by /resources and overview dashboard).
-
const [providersData, scansData] = await Promise.all([
getProviders({ pageSize: 50 }),
getScans({ pageSize: 50 }),
@@ -54,8 +48,10 @@ export default async function Findings({
},
});
+ const hasHistoricalData = hasDateOrScanFilter(filtersWithScanDates);
+
const metadataInfoData = await (
- hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo
+ hasHistoricalData ? getMetadataInfo : getLatestMetadataInfo
)({
query,
sort: encodedSort,
@@ -122,10 +118,9 @@ const SSRDataTable = async ({
const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10);
const { encodedSort } = extractSortAndKey(searchParams);
- // Check if the searchParams contain any date or scan filter
- const hasDateOrScan = hasDateOrScanFilter(searchParams);
+ const hasHistoricalData = hasDateOrScanFilter(filters);
- const fetchFindingGroups = hasDateOrScan
+ const fetchFindingGroups = hasHistoricalData
? getFindingGroups
: getLatestFindingGroups;
@@ -154,7 +149,7 @@ const SSRDataTable = async ({
data={groups}
metadata={findingGroupsData?.meta}
resolvedFilters={filters}
- hasHistoricalData={hasDateOrScan}
+ hasHistoricalData={hasHistoricalData}
/>
>
);
diff --git a/ui/app/(prowler)/providers/page.tsx b/ui/app/(prowler)/providers/page.tsx
index 3520670c32..8cbdfb3459 100644
--- a/ui/app/(prowler)/providers/page.tsx
+++ b/ui/app/(prowler)/providers/page.tsx
@@ -1,11 +1,6 @@
import { Suspense } from "react";
-import {
- AddProviderButton,
- MutedFindingsConfigButton,
- ProvidersAccountsTable,
- ProvidersFilters,
-} from "@/components/providers";
+import { ProvidersAccountsView } from "@/components/providers";
import { SkeletonTableProviders } from "@/components/providers/table";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
import { ContentLayout } from "@/components/ui";
@@ -56,15 +51,6 @@ export default async function Providers({
);
}
-const ProvidersActions = () => {
- return (
-
- );
-};
-
const ProvidersTableFallback = () => {
return (
@@ -120,17 +106,12 @@ const ProvidersAccountsContent = async ({
});
return (
-
+
);
};
diff --git a/ui/components/compliance/compliance-accordion/client-accordion-content.test.ts b/ui/components/compliance/compliance-accordion/client-accordion-content.test.ts
new file mode 100644
index 0000000000..7f724e20b7
--- /dev/null
+++ b/ui/components/compliance/compliance-accordion/client-accordion-content.test.ts
@@ -0,0 +1,16 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("client accordion content", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "client-accordion-content.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("uses the shared standalone finding columns instead of the legacy findings columns", () => {
+ expect(source).toContain("getStandaloneFindingColumns");
+ expect(source).not.toContain("getColumnFindings");
+ });
+});
diff --git a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx
index 965df073a1..659d3a83fa 100644
--- a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx
+++ b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx
@@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from "react";
import { getFindings } from "@/actions/findings/findings";
import {
- getColumnFindings,
+ getStandaloneFindingColumns,
SkeletonTableFindings,
} from "@/components/findings/table";
import { Accordion } from "@/components/ui/accordion/Accordion";
@@ -33,6 +33,7 @@ export const ClientAccordionContent = ({
const searchParams = useSearchParams();
const pageNumber = searchParams.get("page") || "1";
const complianceId = searchParams.get("complianceId");
+ const openFindingId = searchParams.get("id");
const defaultSort = "severity,status,-inserted_at";
const sort = searchParams.get("sort") || defaultSort;
const loadedPageRef = useRef
(null);
@@ -61,6 +62,7 @@ export const ClientAccordionContent = ({
filters: {
"filter[check_id__in]": checkIds.join(","),
"filter[scan]": scanId,
+ "filter[muted]": "false",
...(region && { "filter[region__in]": region }),
},
page: parseInt(pageNumber, 10),
@@ -159,12 +161,7 @@ export const ClientAccordionContent = ({
Findings
- col.id !== "select" &&
- !("accessorKey" in col && col.accessorKey === "updated_at"),
- )}
+ columns={getStandaloneFindingColumns({ openFindingId })}
data={expandedFindings || []}
metadata={findings?.meta}
disableScroll={true}
diff --git a/ui/components/compliance/compliance-card.test.tsx b/ui/components/compliance/compliance-card.test.tsx
new file mode 100644
index 0000000000..c7a199a7fc
--- /dev/null
+++ b/ui/components/compliance/compliance-card.test.tsx
@@ -0,0 +1,30 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("ComplianceCard", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "compliance-card.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("keeps the shadcn Card base variant", () => {
+ expect(source).toContain('variant="base"');
+ });
+
+ it("uses a responsive stacked layout for narrow screens", () => {
+ expect(source).toContain("flex-col");
+ expect(source).toContain("sm:flex-row");
+ });
+
+ it("uses the shadcn progress component instead of Hero UI", () => {
+ expect(source).toContain('from "@/components/shadcn/progress"');
+ expect(source).not.toContain("@heroui/progress");
+ });
+
+ it("places compact actions in the icon column on larger screens", () => {
+ expect(source).toContain('orientation="column"');
+ expect(source).toContain('buttonWidth="icon"');
+ });
+});
diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx
index 5d1b0425ba..2c9f383f2b 100644
--- a/ui/components/compliance/compliance-card.tsx
+++ b/ui/components/compliance/compliance-card.tsx
@@ -1,11 +1,20 @@
"use client";
-import { Progress } from "@heroui/progress";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { Card, CardContent } from "@/components/shadcn/card/card";
+import { Progress } from "@/components/shadcn/progress";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/shadcn/tooltip";
import { getReportTypeForFramework } from "@/lib/compliance/compliance-report-types";
+import {
+ getScoreIndicatorClass,
+ type ScoreColorVariant,
+} from "@/lib/compliance/score-utils";
import { ScanEntity } from "@/types/scans";
import { getComplianceIcon } from "../icons";
@@ -45,13 +54,9 @@ export const ComplianceCard: React.FC = ({
(passingRequirements / totalRequirements) * 100,
);
- const getRatingColor = (ratingPercentage: number) => {
- if (ratingPercentage <= 10) {
- return "danger";
- }
- if (ratingPercentage <= 40) {
- return "warning";
- }
+ const getRatingVariant = (value: number): ScoreColorVariant => {
+ if (value <= 10) return "danger";
+ if (value <= 40) return "warning";
return "success";
};
@@ -80,58 +85,76 @@ export const ComplianceCard: React.FC = ({
onClick={navigateToDetail}
>
-
- {getComplianceIcon(title) && (
-
- )}
-
-
- {formatTitle(title)}
- {version ? ` - ${version}` : ""}
-
-
+
+ {getComplianceIcon(title) && (
+
+ )}
+
e.stopPropagation()}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.stopPropagation();
+ }
}}
- color={getRatingColor(ratingPercentage)}
- />
-
-
+ role="group"
+ tabIndex={0}
+ >
+
+
+
+
+
+
+
+ {formatTitle(title)}
+ {version ? ` - ${version}` : ""}
+
+
+
+ {formatTitle(title)}
+ {version ? ` - ${version}` : ""}
+
+
+
+
+
+ Score:
+
+
+ {ratingPercentage}%
+
+
+
+
+
+
{passingRequirements} / {totalRequirements}
Passing Requirements
-
-
e.stopPropagation()}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.stopPropagation();
- }
- }}
- role="group"
- tabIndex={0}
- >
-
-
diff --git a/ui/components/compliance/compliance-download-container.test.tsx b/ui/components/compliance/compliance-download-container.test.tsx
new file mode 100644
index 0000000000..4a13c35fbc
--- /dev/null
+++ b/ui/components/compliance/compliance-download-container.test.tsx
@@ -0,0 +1,133 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const { downloadComplianceCsvMock, downloadComplianceReportPdfMock } =
+ vi.hoisted(() => ({
+ downloadComplianceCsvMock: vi.fn(),
+ downloadComplianceReportPdfMock: vi.fn(),
+ }));
+
+vi.mock("@/lib/helper", () => ({
+ downloadComplianceCsv: downloadComplianceCsvMock,
+ downloadComplianceReportPdf: downloadComplianceReportPdfMock,
+}));
+
+vi.mock("@/components/ui", () => ({
+ toast: {},
+}));
+
+import { ComplianceDownloadContainer } from "./compliance-download-container";
+
+describe("ComplianceDownloadContainer", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "compliance-download-container.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("uses the shared action dropdown for the card actions mode", () => {
+ expect(source).toContain("ActionDropdown");
+ expect(source).not.toContain("@heroui/button");
+ });
+
+ it("should expose an accessible actions menu trigger", () => {
+ render(
+ ,
+ );
+
+ expect(
+ screen.getByRole("button", { name: "Open compliance export actions" }),
+ ).toBeInTheDocument();
+ });
+
+ it("should support fixed icon-sized dropdown trigger in column mode", () => {
+ render(
+ ,
+ );
+
+ const trigger = screen.getByRole("button", {
+ name: "Open compliance export actions",
+ });
+ expect(trigger.className).toContain("border-text-neutral-secondary");
+ });
+
+ it("should open export actions from the compact trigger", async () => {
+ const user = userEvent.setup();
+
+ render(
+ ,
+ );
+
+ await user.click(
+ screen.getByRole("button", { name: "Open compliance export actions" }),
+ );
+
+ expect(screen.getByText("Download CSV report")).toBeInTheDocument();
+ expect(screen.getByText("Download PDF report")).toBeInTheDocument();
+ });
+
+ it("should trigger both downloads from the actions menu", async () => {
+ const user = userEvent.setup();
+
+ render(
+ ,
+ );
+
+ await user.click(
+ screen.getByRole("button", { name: "Open compliance export actions" }),
+ );
+ await user.click(
+ screen.getByRole("menuitem", { name: /Download CSV report/i }),
+ );
+ await user.click(
+ screen.getByRole("button", { name: "Open compliance export actions" }),
+ );
+ await user.click(
+ screen.getByRole("menuitem", { name: /Download PDF report/i }),
+ );
+
+ expect(downloadComplianceCsvMock).toHaveBeenCalledWith(
+ "scan-1",
+ "compliance-1",
+ {},
+ );
+ expect(downloadComplianceReportPdfMock).toHaveBeenCalledWith(
+ "scan-1",
+ "threatscore",
+ {},
+ );
+ });
+});
diff --git a/ui/components/compliance/compliance-download-container.tsx b/ui/components/compliance/compliance-download-container.tsx
index 526057ea5c..415da1d85a 100644
--- a/ui/components/compliance/compliance-download-container.tsx
+++ b/ui/components/compliance/compliance-download-container.tsx
@@ -4,6 +4,15 @@ import { DownloadIcon, FileTextIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/shadcn/button/button";
+import {
+ ActionDropdown,
+ ActionDropdownItem,
+} from "@/components/shadcn/dropdown";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/shadcn/tooltip";
import { toast } from "@/components/ui";
import type { ComplianceReportType } from "@/lib/compliance/compliance-report-types";
import {
@@ -18,6 +27,9 @@ interface ComplianceDownloadContainerProps {
reportType?: ComplianceReportType;
compact?: boolean;
disabled?: boolean;
+ orientation?: "row" | "column";
+ buttonWidth?: "auto" | "icon";
+ presentation?: "buttons" | "dropdown";
}
export const ComplianceDownloadContainer = ({
@@ -26,9 +38,14 @@ export const ComplianceDownloadContainer = ({
reportType,
compact = false,
disabled = false,
+ orientation = "row",
+ buttonWidth = "auto",
+ presentation = "buttons",
}: ComplianceDownloadContainerProps) => {
const [isDownloadingCsv, setIsDownloadingCsv] = useState(false);
const [isDownloadingPdf, setIsDownloadingPdf] = useState(false);
+ const isIconWidth = buttonWidth === "icon";
+ const isDropdown = presentation === "dropdown";
const handleDownloadCsv = async () => {
if (isDownloadingCsv) return;
@@ -52,40 +69,116 @@ export const ComplianceDownloadContainer = ({
const buttonClassName = cn(
"border-button-primary text-button-primary hover:bg-button-primary/10",
- compact && "h-7 px-2 text-xs",
+ compact &&
+ !isIconWidth &&
+ "h-7 px-2 text-xs sm:w-full sm:justify-center sm:px-2.5",
+ orientation === "column" && !isIconWidth && "w-full",
+ isIconWidth && "size-10 rounded-lg p-0",
);
+ const labelClassName = isIconWidth
+ ? "sr-only"
+ : compact
+ ? "sr-only sm:not-sr-only"
+ : undefined;
+ const showTooltip = compact || isIconWidth;
return (
-
-
-
- CSV
-
- {reportType && (
-
+ {isDropdown ? (
+
-
+ }
+ label="Download CSV report"
+ onSelect={handleDownloadCsv}
+ disabled={disabled || isDownloadingCsv}
/>
- PDF
-
+ {reportType && (
+
+ }
+ label="Download PDF report"
+ onSelect={handleDownloadPdf}
+ disabled={disabled || isDownloadingPdf}
+ />
+ )}
+
+ ) : (
+
+
+
+
+
+ CSV
+
+
+ {showTooltip && (
+ Download CSV report
+ )}
+
+ {reportType && (
+
+
+
+
+ PDF
+
+
+ {showTooltip && (
+ Download PDF report
+ )}
+
+ )}
+
)}
);
diff --git a/ui/components/compliance/compliance-header/compliance-filters.tsx b/ui/components/compliance/compliance-header/compliance-filters.tsx
new file mode 100644
index 0000000000..474b61d4e7
--- /dev/null
+++ b/ui/components/compliance/compliance-header/compliance-filters.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+
+import { ClearFiltersButton } from "@/components/filters/clear-filters-button";
+import {
+ MultiSelect,
+ MultiSelectContent,
+ MultiSelectItem,
+ MultiSelectSelectAll,
+ MultiSelectSeparator,
+ MultiSelectTrigger,
+ MultiSelectValue,
+} from "@/components/shadcn/select/multiselect";
+import { useUrlFilters } from "@/hooks/use-url-filters";
+
+import { ScanSelector, SelectScanComplianceDataProps } from "./scan-selector";
+
+interface ComplianceFiltersProps {
+ scans: SelectScanComplianceDataProps["scans"];
+ uniqueRegions: string[];
+ selectedScanId: string;
+}
+
+export const ComplianceFilters = ({
+ scans,
+ uniqueRegions,
+ selectedScanId,
+}: ComplianceFiltersProps) => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const { updateFilter } = useUrlFilters();
+
+ const handleScanChange = (selectedKey: string) => {
+ const params = new URLSearchParams(searchParams);
+ params.set("scanId", selectedKey);
+ router.push(`?${params.toString()}`, { scroll: false });
+ };
+
+ const regionValues =
+ searchParams.get("filter[region__in]")?.split(",").filter(Boolean) ?? [];
+
+ return (
+
+
+
+
+ {uniqueRegions.length > 0 && (
+
+ updateFilter("region__in", values)}
+ >
+
+
+
+
+ Select All
+
+ {uniqueRegions.map((region) => (
+
+ {region}
+
+ ))}
+
+
+
+ )}
+
+
+ );
+};
diff --git a/ui/components/compliance/compliance-header/compliance-header.test.tsx b/ui/components/compliance/compliance-header/compliance-header.test.tsx
new file mode 100644
index 0000000000..b646199f7f
--- /dev/null
+++ b/ui/components/compliance/compliance-header/compliance-header.test.tsx
@@ -0,0 +1,18 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("ComplianceHeader", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "compliance-header.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("renders the scan selector inside the shared filters grid using default layout", () => {
+ expect(source).toContain("prependElement");
+ expect(source).toContain(" {
const frameworkFilters = [];
+ const prependElement = showProviders ? (
+
+ ) : undefined;
// Add CIS Profile Level filter if framework is CIS
if (framework === "CIS") {
@@ -42,6 +45,7 @@ export const ComplianceHeader = ({
key: "cis_profile_level",
labelCheckboxGroup: "Level",
values: ["Level 1", "Level 2"],
+ width: "wide" as const,
index: 0, // Show first
showSelectAll: false, // No "Select All" option since Level 2 includes Level 1
defaultValues: ["Level 2"], // Default to Level 2 selected (which includes Level 1)
@@ -55,6 +59,7 @@ export const ComplianceHeader = ({
key: "region__in",
labelCheckboxGroup: "Regions",
values: uniqueRegions,
+ width: "wide" as const,
index: 1, // Show after framework filters
},
]
@@ -77,9 +82,11 @@ export const ComplianceHeader = ({
{selectedScan && }
{/* Showed in the compliance page */}
- {showProviders && }
- {!hideFilters && allFilters.length > 0 && (
-
+ {!hideFilters && (allFilters.length > 0 || showProviders) && (
+
)}
{logoPath && complianceTitle && (
diff --git a/ui/components/compliance/compliance-header/data-compliance.tsx b/ui/components/compliance/compliance-header/data-compliance.tsx
index 992dc3d683..4787d00921 100644
--- a/ui/components/compliance/compliance-header/data-compliance.tsx
+++ b/ui/components/compliance/compliance-header/data-compliance.tsx
@@ -7,11 +7,13 @@ import {
ScanSelector,
SelectScanComplianceDataProps,
} from "@/components/compliance/compliance-header/index";
+import { cn } from "@/lib/utils";
interface DataComplianceProps {
scans: SelectScanComplianceDataProps["scans"];
+ className?: string;
}
-export const DataCompliance = ({ scans }: DataComplianceProps) => {
+export const DataCompliance = ({ scans, className }: DataComplianceProps) => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -36,7 +38,7 @@ export const DataCompliance = ({ scans }: DataComplianceProps) => {
};
return (
-
+
{
const selectedScan = scans.find((item) => item.id === selectedScanId);
+ const triggerLabel = selectedScan ? getScanEntityLabel(selectedScan) : "";
return (
-
+
{selectedScan ? (
-
+ <>
+
+ Scan:
+
+
+ {triggerLabel}
+
+ >
) : (
"Select a scan"
)}
-
+
{scans.map((scan) => (
diff --git a/ui/components/compliance/compliance-overview-grid.tsx b/ui/components/compliance/compliance-overview-grid.tsx
new file mode 100644
index 0000000000..2929311a70
--- /dev/null
+++ b/ui/components/compliance/compliance-overview-grid.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import { useState } from "react";
+
+import { ComplianceCard } from "@/components/compliance/compliance-card";
+import { DataTableSearch } from "@/components/ui/table/data-table-search";
+import type { ComplianceOverviewData } from "@/types/compliance";
+import type { ScanEntity } from "@/types/scans";
+
+interface ComplianceOverviewGridProps {
+ frameworks: ComplianceOverviewData[];
+ scanId: string;
+ selectedScan?: ScanEntity;
+}
+
+export const ComplianceOverviewGrid = ({
+ frameworks,
+ scanId,
+ selectedScan,
+}: ComplianceOverviewGridProps) => {
+ const [searchTerm, setSearchTerm] = useState("");
+
+ const filteredFrameworks = frameworks.filter((compliance) =>
+ compliance.attributes.framework
+ .toLowerCase()
+ .includes(searchTerm.toLowerCase()),
+ );
+
+ return (
+ <>
+
+
+
+ {filteredFrameworks.length.toLocaleString()} Total Entries
+
+
+
+ {filteredFrameworks.map((compliance) => {
+ const { attributes, id } = compliance;
+ const {
+ framework,
+ version,
+ requirements_passed,
+ total_requirements,
+ } = attributes;
+
+ return (
+
+ );
+ })}
+
+ >
+ );
+};
diff --git a/ui/components/compliance/index.ts b/ui/components/compliance/index.ts
index 3cff13df45..e0bd631200 100644
--- a/ui/components/compliance/index.ts
+++ b/ui/components/compliance/index.ts
@@ -13,10 +13,12 @@ export * from "./compliance-custom-details/cis-details";
export * from "./compliance-custom-details/ens-details";
export * from "./compliance-custom-details/iso-details";
export * from "./compliance-download-container";
+export * from "./compliance-header/compliance-filters";
export * from "./compliance-header/compliance-header";
export * from "./compliance-header/compliance-scan-info";
export * from "./compliance-header/data-compliance";
export * from "./compliance-header/scan-selector";
+export * from "./compliance-overview-grid";
export * from "./no-scans-available";
export * from "./skeletons/bar-chart-skeleton";
export * from "./skeletons/compliance-accordion-skeleton";
diff --git a/ui/components/compliance/skeletons/compliance-grid-skeleton.test.tsx b/ui/components/compliance/skeletons/compliance-grid-skeleton.test.tsx
new file mode 100644
index 0000000000..c7621aab04
--- /dev/null
+++ b/ui/components/compliance/skeletons/compliance-grid-skeleton.test.tsx
@@ -0,0 +1,17 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("ComplianceSkeletonGrid", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "compliance-grid-skeleton.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("uses shadcn skeletons instead of Hero UI", () => {
+ expect(source).toContain('from "@/components/shadcn/skeleton/skeleton"');
+ expect(source).not.toContain("@heroui/card");
+ expect(source).not.toContain("@heroui/skeleton");
+ });
+});
diff --git a/ui/components/compliance/skeletons/compliance-grid-skeleton.tsx b/ui/components/compliance/skeletons/compliance-grid-skeleton.tsx
index 77631da207..dfba84b00f 100644
--- a/ui/components/compliance/skeletons/compliance-grid-skeleton.tsx
+++ b/ui/components/compliance/skeletons/compliance-grid-skeleton.tsx
@@ -1,19 +1,11 @@
-import { Card } from "@heroui/card";
-import { Skeleton } from "@heroui/skeleton";
-import React from "react";
+import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
export const ComplianceSkeletonGrid = () => {
return (
-
-
- {[...Array(28)].map((_, index) => (
-
- ))}
-
-
+
+ {[...Array(28)].map((_, index) => (
+
+ ))}
+
);
};
diff --git a/ui/components/compliance/threatscore-badge.test.tsx b/ui/components/compliance/threatscore-badge.test.tsx
new file mode 100644
index 0000000000..2151f9bbf0
--- /dev/null
+++ b/ui/components/compliance/threatscore-badge.test.tsx
@@ -0,0 +1,32 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("ThreatScoreBadge", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "threatscore-badge.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("uses shadcn card and progress components instead of Hero UI", () => {
+ expect(source).toContain('from "@/components/shadcn/card/card"');
+ expect(source).toContain('from "@/components/shadcn/progress"');
+ expect(source).not.toContain("@heroui/card");
+ expect(source).not.toContain("@heroui/progress");
+ });
+
+ it("uses ActionDropdown for downloads instead of ComplianceDownloadContainer", () => {
+ expect(source).toContain("ActionDropdown");
+ expect(source).toContain("ActionDropdownItem");
+ expect(source).toContain("downloadComplianceCsv");
+ expect(source).toContain("downloadComplianceReportPdf");
+ expect(source).not.toContain("ComplianceDownloadContainer");
+ });
+
+ it("does not use Collapsible components", () => {
+ expect(source).not.toContain("Collapsible");
+ expect(source).not.toContain("CollapsibleTrigger");
+ expect(source).not.toContain("CollapsibleContent");
+ });
+});
diff --git a/ui/components/compliance/threatscore-badge.tsx b/ui/components/compliance/threatscore-badge.tsx
index feb2aec91b..89645041c6 100644
--- a/ui/components/compliance/threatscore-badge.tsx
+++ b/ui/components/compliance/threatscore-badge.tsx
@@ -1,27 +1,24 @@
"use client";
-import { Card, CardBody } from "@heroui/card";
-import { Progress } from "@heroui/progress";
-import {
- ChevronDown,
- ChevronUp,
- DownloadIcon,
- FileTextIcon,
-} from "lucide-react";
+import { DownloadIcon, FileTextIcon } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import type { SectionScores } from "@/actions/overview/threat-score";
import { ThreatScoreLogo } from "@/components/compliance/threatscore-logo";
-import { Button } from "@/components/shadcn/button/button";
+import { Card, CardContent } from "@/components/shadcn/card/card";
import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from "@/components/shadcn/collapsible";
+ ActionDropdown,
+ ActionDropdownItem,
+} from "@/components/shadcn/dropdown";
+import { Progress } from "@/components/shadcn/progress";
import { toast } from "@/components/ui";
import { COMPLIANCE_REPORT_TYPES } from "@/lib/compliance/compliance-report-types";
-import { getScoreColor, getScoreTextClass } from "@/lib/compliance/score-utils";
+import {
+ getScoreColor,
+ getScoreIndicatorClass,
+ getScoreTextClass,
+} from "@/lib/compliance/score-utils";
import {
downloadComplianceCsv,
downloadComplianceReportPdf,
@@ -44,9 +41,8 @@ export const ThreatScoreBadge = ({
}: ThreatScoreBadgeProps) => {
const router = useRouter();
const searchParams = useSearchParams();
- const [isDownloadingPdf, setIsDownloadingPdf] = useState(false);
const [isDownloadingCsv, setIsDownloadingCsv] = useState(false);
- const [isExpanded, setIsExpanded] = useState(false);
+ const [isDownloadingPdf, setIsDownloadingPdf] = useState(false);
const complianceId = `prowler_threatscore_${provider.toLowerCase()}`;
@@ -69,7 +65,18 @@ export const ThreatScoreBadge = ({
router.push(`${path}?${params.toString()}`);
};
+ const handleDownloadCsv = async () => {
+ if (isDownloadingCsv) return;
+ setIsDownloadingCsv(true);
+ try {
+ await downloadComplianceCsv(scanId, complianceId, toast);
+ } finally {
+ setIsDownloadingCsv(false);
+ }
+ };
+
const handleDownloadPdf = async () => {
+ if (isDownloadingPdf) return;
setIsDownloadingPdf(true);
try {
await downloadComplianceReportPdf(
@@ -82,23 +89,12 @@ export const ThreatScoreBadge = ({
}
};
- const handleDownloadCsv = async () => {
- setIsDownloadingCsv(true);
- try {
- await downloadComplianceCsv(scanId, complianceId, toast);
- } finally {
- setIsDownloadingCsv(false);
- }
- };
-
return (
-
-
+
+
+ {/* Clickable ThreatScore button */}
@@ -111,92 +107,67 @@ export const ThreatScoreBadge = ({
+ {/* Pillar breakdown — always visible */}
{sectionScores && Object.keys(sectionScores).length > 0 && (
-
-
- {isExpanded ? (
- <>
-
- Hide pillar breakdown
- >
- ) : (
- <>
-
- Show pillar breakdown
- >
- )}
-
-
- {Object.entries(sectionScores)
- .sort(([, a], [, b]) => a - b)
- .map(([section, sectionScore]) => (
-
+ {Object.entries(sectionScores)
+ .sort(([, a], [, b]) => a - b)
+ .map(([section, sectionScore]) => (
+
+
+ {section}
+
+
+
-
- {section}
-
-
-
- {sectionScore.toFixed(1)}%
-
-
- ))}
-
-
+ {sectionScore.toFixed(1)}%
+
+
+ ))}
+
)}
+
-
-
-
- PDF
-
-
-
- CSV
-
-
-
+ {/* ActionDropdown for downloads — top-right */}
+
+
+
+ }
+ label="Download CSV report"
+ onSelect={handleDownloadCsv}
+ />
+
+ }
+ label="Download PDF report"
+ onSelect={handleDownloadPdf}
+ />
+
+
);
};
diff --git a/ui/components/filters/custom-date-picker.tsx b/ui/components/filters/custom-date-picker.tsx
index 3e92615b5c..effe2ef144 100644
--- a/ui/components/filters/custom-date-picker.tsx
+++ b/ui/components/filters/custom-date-picker.tsx
@@ -12,6 +12,7 @@ import {
PopoverTrigger,
} from "@/components/shadcn/popover";
import { useUrlFilters } from "@/hooks/use-url-filters";
+import { toLocalDateString } from "@/lib/date-utils";
import { cn } from "@/lib/utils";
/** Batch mode: caller controls both the pending date value and the notification callback (all-or-nothing). */
@@ -67,17 +68,14 @@ export const CustomDatePicker = ({
const applyDateFilter = (selectedDate: Date | undefined) => {
if (onBatchChange) {
// Batch mode: notify caller instead of updating URL
- onBatchChange(
- "inserted_at",
- selectedDate ? format(selectedDate, "yyyy-MM-dd") : "",
- );
+ onBatchChange("inserted_at", toLocalDateString(selectedDate) ?? "");
return;
}
// Instant mode (default): push to URL immediately
if (selectedDate) {
// Format as YYYY-MM-DD for the API
- updateFilter("inserted_at", format(selectedDate, "yyyy-MM-dd"));
+ updateFilter("inserted_at", toLocalDateString(selectedDate) ?? "");
} else {
updateFilter("inserted_at", null);
}
diff --git a/ui/components/findings/findings-filters.tsx b/ui/components/findings/findings-filters.tsx
index 6316e49203..57d2711166 100644
--- a/ui/components/findings/findings-filters.tsx
+++ b/ui/components/findings/findings-filters.tsx
@@ -20,10 +20,13 @@ import { DataTableFilterCustom } from "@/components/ui/table";
import { useFilterBatch } from "@/hooks/use-filter-batch";
import { getCategoryLabel, getGroupLabel } from "@/lib/categories";
import { FilterType, ScanEntity } from "@/types";
-import { DATA_TABLE_FILTER_MODE, FilterParam } from "@/types/filters";
+import { DATA_TABLE_FILTER_MODE } from "@/types/filters";
import { ProviderProps } from "@/types/providers";
-import { getFindingsFilterDisplayValue } from "./findings-filters.utils";
+import {
+ buildFindingsFilterChips,
+ getFindingsFilterDisplayValue,
+} from "./findings-filters.utils";
interface FindingsFiltersProps {
/** Provider data for ProviderTypeSelector and AccountsSelector */
@@ -37,30 +40,6 @@ interface FindingsFiltersProps {
uniqueGroups: string[];
}
-/**
- * Maps raw filter param keys (e.g. "filter[severity__in]") to human-readable labels.
- * Used to render chips in the FilterSummaryStrip.
- * Typed as Record
so TypeScript enforces exhaustiveness — any
- * addition to FilterParam will cause a compile error here if the label is missing.
- */
-const FILTER_KEY_LABELS: Record = {
- "filter[provider_type__in]": "Provider",
- "filter[provider_id__in]": "Account",
- "filter[severity__in]": "Severity",
- "filter[status__in]": "Status",
- "filter[delta__in]": "Delta",
- "filter[region__in]": "Region",
- "filter[service__in]": "Service",
- "filter[resource_type__in]": "Resource Type",
- "filter[category__in]": "Category",
- "filter[resource_groups__in]": "Resource Group",
- "filter[scan__in]": "Scan",
- "filter[scan_id]": "Scan",
- "filter[scan_id__in]": "Scan",
- "filter[inserted_at]": "Date",
- "filter[muted]": "Muted",
-};
-
export const FindingsFilters = ({
providers,
completedScanIds,
@@ -132,6 +111,7 @@ export const FindingsFilters = ({
key: FilterType.SCAN,
labelCheckboxGroup: "Scan ID",
values: completedScanIds,
+ width: "wide" as const,
valueLabelMapping: scanDetails,
labelFormatter: (value: string) =>
getFindingsFilterDisplayValue(`filter[${FilterType.SCAN}]`, value, {
@@ -144,25 +124,9 @@ export const FindingsFilters = ({
const hasCustomFilters = customFilters.length > 0;
- // Build FilterChip[] from pendingFilters — one chip per individual value, not per key.
- // Skip filter[muted]="false" — it is the silent default and should not appear as a chip.
- const filterChips: FilterChip[] = [];
- Object.entries(pendingFilters).forEach(([key, values]) => {
- if (!values || values.length === 0) return;
- const label = FILTER_KEY_LABELS[key as FilterParam] ?? key;
- values.forEach((value) => {
- // Do not show a chip for the default muted=false state
- if (key === "filter[muted]" && value === "false") return;
- filterChips.push({
- key,
- label,
- value,
- displayValue: getFindingsFilterDisplayValue(key, value, {
- providers,
- scans: scanDetails,
- }),
- });
- });
+ const filterChips: FilterChip[] = buildFindingsFilterChips(pendingFilters, {
+ providers,
+ scans: scanDetails,
});
// Handler for removing a single chip: update the pending filter to remove that value.
diff --git a/ui/components/findings/findings-filters.utils.test.ts b/ui/components/findings/findings-filters.utils.test.ts
index b4dc63c85b..9fbd56bf63 100644
--- a/ui/components/findings/findings-filters.utils.test.ts
+++ b/ui/components/findings/findings-filters.utils.test.ts
@@ -3,7 +3,10 @@ import { describe, expect, it } from "vitest";
import { ProviderProps } from "@/types/providers";
import { ScanEntity } from "@/types/scans";
-import { getFindingsFilterDisplayValue } from "./findings-filters.utils";
+import {
+ buildFindingsFilterChips,
+ getFindingsFilterDisplayValue,
+} from "./findings-filters.utils";
function makeProvider(
overrides: Partial & { id: string },
@@ -98,7 +101,7 @@ describe("getFindingsFilterDisplayValue", () => {
it("shows the resolved scan badge label for scan filters instead of formatting the raw scan id", () => {
expect(
getFindingsFilterDisplayValue("filter[scan__in]", "scan-1", { scans }),
- ).toBe("Nightly scan");
+ ).toBe("AWS - Nightly scan");
});
it("normalizes finding statuses for display", () => {
@@ -119,7 +122,17 @@ describe("getFindingsFilterDisplayValue", () => {
);
});
- it("falls back to the scan provider uid when the alias is missing", () => {
+ it("formats the singular delta filter the same as delta__in", () => {
+ // The API registers the filter as `filter[delta]` (exact), not `delta__in`.
+ // Both shapes must resolve to the same human label so chips don't show
+ // the raw "new" going through formatLabel ("NEW" via the 3-letter acronym heuristic).
+ expect(getFindingsFilterDisplayValue("filter[delta]", "new")).toBe("New");
+ expect(getFindingsFilterDisplayValue("filter[delta]", "changed")).toBe(
+ "Changed",
+ );
+ });
+
+ it("uses the provider display name regardless of account alias/uid", () => {
expect(
getFindingsFilterDisplayValue("filter[scan__in]", "scan-2", {
scans: [
@@ -133,17 +146,17 @@ describe("getFindingsFilterDisplayValue", () => {
}),
],
}),
- ).toBe("Weekly scan");
+ ).toBe("AWS - Weekly scan");
});
- it("falls back to the provider alias when the scan name is missing", () => {
+ it("returns only the provider name when the scan name is missing", () => {
expect(
getFindingsFilterDisplayValue("filter[scan__in]", "scan-3", {
scans: [
...scans,
makeScanMap("scan-3", {
providerInfo: {
- provider: "aws",
+ provider: "gcp",
alias: "Fallback Account",
uid: "333333333333",
},
@@ -154,7 +167,7 @@ describe("getFindingsFilterDisplayValue", () => {
}),
],
}),
- ).toBe("Fallback Account");
+ ).toBe("Google Cloud");
});
it("keeps the raw scan value when the scan cannot be resolved", () => {
@@ -185,3 +198,85 @@ describe("getFindingsFilterDisplayValue", () => {
).toBe("2026-04-07");
});
});
+
+describe("buildFindingsFilterChips", () => {
+ it("creates one chip per value with normalized labels", () => {
+ // Given — this is the exact pending state derived from the LinkToFindings URL:
+ // /findings?sort=...&filter[status__in]=FAIL&filter[delta]=new
+ const pendingFilters = {
+ "filter[status__in]": ["FAIL"],
+ "filter[delta]": ["new"],
+ };
+
+ // When
+ const chips = buildFindingsFilterChips(pendingFilters);
+
+ // Then — both chips must appear; the delta chip must use "Delta" as label
+ // (not the raw "filter[delta]") and "New" as displayValue (not "NEW" via
+ // the short-word acronym heuristic in formatLabel).
+ expect(chips).toEqual([
+ {
+ key: "filter[status__in]",
+ label: "Status",
+ value: "FAIL",
+ displayValue: "Fail",
+ },
+ {
+ key: "filter[delta]",
+ label: "Delta",
+ value: "new",
+ displayValue: "New",
+ },
+ ]);
+ });
+
+ it("treats filter[delta] and filter[delta__in] identically", () => {
+ // Given
+ const chipsSingular = buildFindingsFilterChips({
+ "filter[delta]": ["new", "changed"],
+ });
+ const chipsPlural = buildFindingsFilterChips({
+ "filter[delta__in]": ["new", "changed"],
+ });
+
+ // Then — both shapes produce the same human labels and display values
+ expect(
+ chipsSingular.map((c) => ({ label: c.label, v: c.displayValue })),
+ ).toEqual([
+ { label: "Delta", v: "New" },
+ { label: "Delta", v: "Changed" },
+ ]);
+ expect(
+ chipsPlural.map((c) => ({ label: c.label, v: c.displayValue })),
+ ).toEqual([
+ { label: "Delta", v: "New" },
+ { label: "Delta", v: "Changed" },
+ ]);
+ });
+
+ it("skips the silent default filter[muted]=false", () => {
+ const chips = buildFindingsFilterChips({
+ "filter[muted]": ["false"],
+ "filter[delta]": ["new"],
+ });
+
+ // Only the delta chip — the default muted=false should not surface
+ expect(chips).toHaveLength(1);
+ expect(chips[0].key).toBe("filter[delta]");
+ });
+
+ it("surfaces unmapped keys using the raw key as label (fallback)", () => {
+ const chips = buildFindingsFilterChips({
+ "filter[unknown_future_key]": ["value"],
+ });
+
+ expect(chips).toEqual([
+ {
+ key: "filter[unknown_future_key]",
+ label: "filter[unknown_future_key]",
+ value: "value",
+ displayValue: "Value",
+ },
+ ]);
+ });
+});
diff --git a/ui/components/findings/findings-filters.utils.ts b/ui/components/findings/findings-filters.utils.ts
index 2599650c1d..0b8f752f69 100644
--- a/ui/components/findings/findings-filters.utils.ts
+++ b/ui/components/findings/findings-filters.utils.ts
@@ -1,5 +1,8 @@
+import type { FilterChip } from "@/components/filters/filter-summary-strip";
import { formatLabel, getCategoryLabel, getGroupLabel } from "@/lib/categories";
+import { getScanEntityLabel } from "@/lib/helper-filters";
import { FINDING_STATUS_DISPLAY_NAMES } from "@/types";
+import { FilterParam } from "@/types/filters";
import { getProviderDisplayName, ProviderProps } from "@/types/providers";
import { ScanEntity } from "@/types/scans";
import { SEVERITY_DISPLAY_NAMES } from "@/types/severities";
@@ -35,12 +38,7 @@ function getScanDisplayValue(
return scanId;
}
- return (
- scan.attributes.name ||
- scan.providerInfo.alias ||
- scan.providerInfo.uid ||
- scanId
- );
+ return getScanEntityLabel(scan) || scanId;
}
export function getFindingsFilterDisplayValue(
@@ -55,7 +53,7 @@ export function getFindingsFilterDisplayValue(
if (filterKey === "filter[provider_id__in]") {
return getProviderAccountDisplayValue(value, options.providers || []);
}
- if (filterKey === "filter[scan__in]") {
+ if (filterKey === "filter[scan__in]" || filterKey === "filter[scan]") {
return getScanDisplayValue(value, options.scans || []);
}
if (filterKey === "filter[severity__in]") {
@@ -72,7 +70,7 @@ export function getFindingsFilterDisplayValue(
] ?? formatLabel(value)
);
}
- if (filterKey === "filter[delta__in]") {
+ if (filterKey === "filter[delta__in]" || filterKey === "filter[delta]") {
return (
FINDING_DELTA_DISPLAY_NAMES[value.toLowerCase()] ?? formatLabel(value)
);
@@ -93,3 +91,67 @@ export function getFindingsFilterDisplayValue(
return formatLabel(value);
}
+
+/**
+ * Maps raw filter param keys (e.g. "filter[severity__in]") to human-readable labels.
+ * Used to render chips in the FilterSummaryStrip.
+ * Typed as Record so TypeScript enforces exhaustiveness — any
+ * addition to FilterParam will cause a compile error here if the label is missing.
+ */
+export const FILTER_KEY_LABELS: Record = {
+ "filter[provider_type__in]": "Provider",
+ "filter[provider_id__in]": "Account",
+ "filter[severity__in]": "Severity",
+ "filter[status__in]": "Status",
+ "filter[delta__in]": "Delta",
+ "filter[delta]": "Delta",
+ "filter[region__in]": "Region",
+ "filter[service__in]": "Service",
+ "filter[resource_type__in]": "Resource Type",
+ "filter[category__in]": "Category",
+ "filter[resource_groups__in]": "Resource Group",
+ "filter[scan]": "Scan",
+ "filter[scan__in]": "Scan",
+ "filter[scan_id]": "Scan",
+ "filter[scan_id__in]": "Scan",
+ "filter[inserted_at]": "Date",
+ "filter[muted]": "Muted",
+};
+
+interface BuildFindingsFilterChipsOptions {
+ providers?: ProviderProps[];
+ scans?: Array<{ [scanId: string]: ScanEntity }>;
+}
+
+/**
+ * Builds the chips displayed in the FilterSummaryStrip from a pendingFilters map.
+ *
+ * - One chip per individual value (not one per key), so a multi-select filter
+ * produces multiple chips.
+ * - Silently skips the default `filter[muted]=false` so it doesn't appear as a
+ * user-applied filter.
+ * - Falls back to the raw key as label for unmapped keys, so an unexpected
+ * param still surfaces instead of disappearing.
+ */
+export function buildFindingsFilterChips(
+ pendingFilters: Record,
+ options: BuildFindingsFilterChipsOptions = {},
+): FilterChip[] {
+ const chips: FilterChip[] = [];
+
+ Object.entries(pendingFilters).forEach(([key, values]) => {
+ if (!values || values.length === 0) return;
+ const label = FILTER_KEY_LABELS[key as FilterParam] ?? key;
+ values.forEach((value) => {
+ if (key === "filter[muted]" && value === "false") return;
+ chips.push({
+ key,
+ label,
+ value,
+ displayValue: getFindingsFilterDisplayValue(key, value, options),
+ });
+ });
+ });
+
+ return chips;
+}
diff --git a/ui/components/findings/table/column-finding-groups.test.tsx b/ui/components/findings/table/column-finding-groups.test.tsx
index 3f2568db51..eefbcb5a45 100644
--- a/ui/components/findings/table/column-finding-groups.test.tsx
+++ b/ui/components/findings/table/column-finding-groups.test.tsx
@@ -78,6 +78,18 @@ vi.mock("./notification-indicator", () => ({
},
}));
+vi.mock("@/components/shadcn/tooltip", () => ({
+ Tooltip: ({ children }: { children: ReactNode }) => <>{children}>,
+ TooltipContent: ({ children }: { children: ReactNode }) => <>{children}>,
+ TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}>,
+}));
+
+vi.mock("./provider-icon-cell", () => ({
+ ProviderIconCell: ({ provider }: { provider: string }) => (
+ {provider}
+ ),
+}));
+
// ---------------------------------------------------------------------------
// Import after mocks
// ---------------------------------------------------------------------------
@@ -148,6 +160,26 @@ function renderFindingCell(
render({CellComponent({ row: { original: group } })}
);
}
+function renderFindingGroupTitleCell(overrides?: Partial) {
+ const columns = getColumnFindingGroups({
+ rowSelection: {},
+ selectableRowCount: 1,
+ onDrillDown: vi.fn(),
+ });
+
+ const findingColumn = columns.find(
+ (col) => (col as { accessorKey?: string }).accessorKey === "finding",
+ );
+ if (!findingColumn?.cell) throw new Error("finding column not found");
+
+ const group = makeGroup(overrides);
+ const CellComponent = findingColumn.cell as (props: {
+ row: { original: FindingGroupRow };
+ }) => ReactNode;
+
+ render({CellComponent({ row: { original: group } })}
);
+}
+
function renderImpactedResourcesCell(overrides?: Partial) {
const columns = getColumnFindingGroups({
rowSelection: {},
@@ -171,11 +203,13 @@ function renderImpactedResourcesCell(overrides?: Partial) {
}
function renderSelectCell(overrides?: Partial) {
+ const onDrillDown =
+ vi.fn<(checkId: string, group: FindingGroupRow) => void>();
const toggleSelected = vi.fn();
const columns = getColumnFindingGroups({
rowSelection: {},
selectableRowCount: 1,
- onDrillDown: vi.fn(),
+ onDrillDown,
});
const selectColumn = columns.find(
@@ -206,7 +240,7 @@ function renderSelectCell(overrides?: Partial) {
,
);
- return { toggleSelected };
+ return { onDrillDown, toggleSelected };
}
// ---------------------------------------------------------------------------
@@ -231,6 +265,15 @@ describe("column-finding-groups — accessibility of check title cell", () => {
expect(impactedProvidersColumn).toBeUndefined();
});
+ it("should render the first provider icon with its provider name", () => {
+ // Given
+ renderFindingGroupTitleCell({ providers: ["iac"] });
+
+ // Then
+ expect(screen.getByTestId("provider-icon-iac")).toBeInTheDocument();
+ expect(screen.getByText("Infrastructure as Code")).toBeInTheDocument();
+ });
+
it("should render the check title as a button element (not a )", () => {
// Given
const onDrillDown =
@@ -332,6 +375,47 @@ describe("column-finding-groups — accessibility of check title cell", () => {
}),
);
});
+
+ it("should keep zero-resource fallback groups non-clickable even when fallback counts are present", () => {
+ // Given
+ const onDrillDown =
+ vi.fn<(checkId: string, group: FindingGroupRow) => void>();
+
+ renderFindingCell("Fallback IaC Check", onDrillDown, {
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ failCount: 0,
+ passCount: 2,
+ manualCount: 1,
+ });
+
+ // Then
+ expect(
+ screen.queryByRole("button", { name: "Fallback IaC Check" }),
+ ).not.toBeInTheDocument();
+ expect(screen.getByText("Fallback IaC Check")).toBeInTheDocument();
+ expect(onDrillDown).not.toHaveBeenCalled();
+ });
+
+ it("should keep fallback groups non-clickable when the displayed total is zero", () => {
+ // Given
+ const onDrillDown =
+ vi.fn<(checkId: string, group: FindingGroupRow) => void>();
+
+ // When
+ renderFindingCell("No failing findings", onDrillDown, {
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ failCount: 0,
+ passCount: 0,
+ });
+
+ // Then
+ expect(
+ screen.queryByRole("button", { name: "No failing findings" }),
+ ).not.toBeInTheDocument();
+ expect(screen.getByText("No failing findings")).toBeInTheDocument();
+ });
});
describe("column-finding-groups — impacted resources count", () => {
@@ -345,6 +429,36 @@ describe("column-finding-groups — impacted resources count", () => {
// Then
expect(screen.getByText("3/5")).toBeInTheDocument();
});
+
+ it("should fall back to finding counts when resources total is zero", () => {
+ // Given/When
+ renderImpactedResourcesCell({
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ failCount: 3,
+ passCount: 2,
+ muted: false,
+ });
+
+ // Then
+ expect(screen.getByText("3/5")).toBeInTheDocument();
+ });
+
+ it("should include muted findings in the denominator when the row is muted", () => {
+ // Given/When
+ renderImpactedResourcesCell({
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ failCount: 3,
+ passCount: 2,
+ failMutedCount: 4,
+ passMutedCount: 1,
+ muted: true,
+ });
+
+ // Then
+ expect(screen.getByText("3/10")).toBeInTheDocument();
+ });
});
describe("column-finding-groups — group selection", () => {
@@ -357,6 +471,42 @@ describe("column-finding-groups — group selection", () => {
expect(screen.getByRole("checkbox", { name: "Select row" })).toBeDisabled();
});
+
+ it("should hide the chevron for zero-resource fallback groups even when fallback counts are present", () => {
+ // Given
+ const { onDrillDown } = renderSelectCell({
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ failCount: 0,
+ passCount: 2,
+ manualCount: 1,
+ });
+
+ // Then
+ expect(
+ screen.queryByRole("button", {
+ name: "Expand S3 Bucket Public Access",
+ }),
+ ).not.toBeInTheDocument();
+ expect(onDrillDown).not.toHaveBeenCalled();
+ });
+
+ it("should hide the chevron for zero-resource groups when the displayed total is zero", () => {
+ // Given/When
+ renderSelectCell({
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ failCount: 0,
+ passCount: 0,
+ });
+
+ // Then
+ expect(
+ screen.queryByRole("button", {
+ name: "Expand S3 Bucket Public Access",
+ }),
+ ).not.toBeInTheDocument();
+ });
});
describe("column-finding-groups — indicators", () => {
diff --git a/ui/components/findings/table/column-finding-groups.tsx b/ui/components/findings/table/column-finding-groups.tsx
index ffd6f18845..79b584edcc 100644
--- a/ui/components/findings/table/column-finding-groups.tsx
+++ b/ui/components/findings/table/column-finding-groups.tsx
@@ -4,6 +4,11 @@ import { ColumnDef, RowSelectionState } from "@tanstack/react-table";
import { ChevronRight } from "lucide-react";
import { Checkbox } from "@/components/shadcn";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/shadcn/tooltip";
import {
DataTableColumnHeader,
SeverityBadge,
@@ -11,15 +16,19 @@ import {
} from "@/components/ui/table";
import { cn } from "@/lib";
import {
+ canDrillDownFindingGroup,
getFilteredFindingGroupDelta,
+ getFindingGroupImpactedCounts,
isFindingGroupMuted,
} from "@/lib/findings-groups";
import { FindingGroupRow } from "@/types";
+import { getProviderDisplayName } from "@/types/providers";
import { DataTableRowActions } from "./data-table-row-actions";
import { canMuteFindingGroup } from "./finding-group-selection";
import { ImpactedResourcesCell } from "./impacted-resources-cell";
-import { DeltaValues, NotificationIndicator } from "./notification-indicator";
+import { NotificationIndicator } from "./notification-indicator";
+import { ProviderIconCell } from "./provider-icon-cell";
interface GetColumnFindingGroupsOptions {
rowSelection: RowSelectionState;
@@ -83,14 +92,7 @@ export function getColumnFindingGroups({
const allMuted = isFindingGroupMuted(group);
const isExpanded = expandedCheckId === group.checkId;
const deltaKey = getFilteredFindingGroupDelta(group, filters);
- const delta =
- deltaKey === "new"
- ? DeltaValues.NEW
- : deltaKey === "changed"
- ? DeltaValues.CHANGED
- : DeltaValues.NONE;
-
- const canExpand = group.resourcesTotal > 0;
+ const canExpand = canDrillDownFindingGroup(group);
const canSelect = canMuteFindingGroup({
resourcesFail: group.resourcesFail,
resourcesTotal: group.resourcesTotal,
@@ -101,7 +103,7 @@ export function getColumnFindingGroups({
return (
@@ -175,23 +177,43 @@ export function getColumnFindingGroups({
),
cell: ({ row }) => {
const group = row.original;
- const canExpand = group.resourcesTotal > 0;
+ const canExpand = canDrillDownFindingGroup(group);
+ const provider = group.providers[0];
+ const providerName = provider
+ ? getProviderDisplayName(provider)
+ : undefined;
return (
-
- {canExpand ? (
-
onDrillDown(group.checkId, group)}
- >
- {group.checkTitle}
-
- ) : (
-
- {group.checkTitle}
-
- )}
+
+ {provider && providerName ? (
+
+
+
+
+ {providerName}
+
+ ) : null}
+
+ {canExpand ? (
+ onDrillDown(group.checkId, group)}
+ >
+ {group.checkTitle}
+
+ ) : (
+
+ {group.checkTitle}
+
+ )}
+
);
},
@@ -216,10 +238,11 @@ export function getColumnFindingGroups({
),
cell: ({ row }) => {
const group = row.original;
+ const counts = getFindingGroupImpactedCounts(group);
return (
);
},
diff --git a/ui/components/findings/table/column-findings.tsx b/ui/components/findings/table/column-findings.tsx
deleted file mode 100644
index f6300e2b73..0000000000
--- a/ui/components/findings/table/column-findings.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-// TODO: Legacy columns — used by overview dashboard (column-new-findings-to-date.tsx).
-// Migrate that consumer to grouped view columns, then delete this file.
-"use client";
-
-import { ColumnDef, RowSelectionState } from "@tanstack/react-table";
-import { Database } from "lucide-react";
-import { useSearchParams } from "next/navigation";
-
-import { FindingDetail } from "@/components/findings/table";
-import { DataTableRowActions } from "@/components/findings/table";
-import { Checkbox } from "@/components/shadcn";
-import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
-import { DateWithTime } from "@/components/ui/entities";
-import {
- DataTableColumnHeader,
- SeverityBadge,
- StatusFindingBadge,
-} from "@/components/ui/table";
-import { FindingProps, ProviderType } from "@/types";
-
-// TODO: PROWLER-379 - Enable ImpactedResourcesCell when backend supports grouped findings
-// import { ImpactedResourcesCell } from "./impacted-resources-cell";
-import { DeltaValues, NotificationIndicator } from "./notification-indicator";
-import { ProviderIconCell } from "./provider-icon-cell";
-
-const getFindingsData = (row: { original: FindingProps }) => {
- return row.original;
-};
-
-const getFindingsMetadata = (row: { original: FindingProps }) => {
- return row.original.attributes.check_metadata;
-};
-
-const getResourceData = (
- row: { original: FindingProps },
- field: keyof FindingProps["relationships"]["resource"]["attributes"],
-) => {
- return row.original.relationships?.resource?.attributes?.[field] || "-";
-};
-
-const getProviderData = (
- row: { original: FindingProps },
- field: keyof FindingProps["relationships"]["provider"]["attributes"],
-) => {
- return row.original.relationships?.provider?.attributes?.[field] || "-";
-};
-
-// Component for finding title that opens the detail drawer
-const FindingTitleCell = ({ row }: { row: { original: FindingProps } }) => {
- const searchParams = useSearchParams();
- const findingId = searchParams.get("id");
- const isOpen = findingId === row.original.id;
- const { checktitle } = row.original.attributes.check_metadata;
-
- return (
-
-
- {checktitle}
-
-
- }
- />
- );
-};
-
-// Function to generate columns with access to selection state
-export function getColumnFindings(
- rowSelection: RowSelectionState,
- selectableRowCount: number,
-): ColumnDef
[] {
- // Calculate selection state from rowSelection for header checkbox
- const selectedCount = Object.values(rowSelection).filter(Boolean).length;
- const isAllSelected =
- selectedCount > 0 && selectedCount === selectableRowCount;
- const isSomeSelected =
- selectedCount > 0 && selectedCount < selectableRowCount;
-
- return [
- // Notification column - shows new/changed/muted indicators
- {
- id: "notification",
- header: () => null,
- cell: ({ row }) => {
- const finding = row.original;
- const isMuted = finding.attributes.muted;
- const mutedReason = finding.attributes.muted_reason;
- const delta = finding.attributes.delta as
- | (typeof DeltaValues)[keyof typeof DeltaValues]
- | undefined;
-
- return (
-
- );
- },
- enableSorting: false,
- enableHiding: false,
- },
- // Select column
- {
- id: "select",
- header: ({ table }) => {
- const headerChecked = isAllSelected
- ? true
- : isSomeSelected
- ? "indeterminate"
- : false;
-
- return (
-
-
- table.toggleAllPageRowsSelected(checked === true)
- }
- aria-label="Select all"
- disabled={selectableRowCount === 0}
- />
-
- );
- },
- cell: ({ row }) => {
- const finding = row.original;
- const isMuted = finding.attributes.muted;
- const isSelected = !!rowSelection[row.id];
-
- return (
-
-
- row.toggleSelected(checked === true)
- }
- aria-label="Select row"
- />
-
- );
- },
- enableSorting: false,
- enableHiding: false,
- },
- // Status column
- {
- accessorKey: "status",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const {
- attributes: { status },
- } = getFindingsData(row);
-
- return ;
- },
- },
- // Finding column - clickable to open detail sheet
- {
- accessorKey: "check",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => ,
- },
- // Resource name column
- {
- accessorKey: "resourceName",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const resourceName = getResourceData(row, "name");
-
- if (resourceName === "-") {
- return -
;
- }
-
- return (
- `...${value.slice(-10)}`}
- icon={ }
- />
- );
- },
- enableSorting: false,
- },
- // Severity column
- {
- accessorKey: "severity",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const {
- attributes: { severity },
- } = getFindingsData(row);
- return ;
- },
- },
- // Provider column
- {
- accessorKey: "provider",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const provider = getProviderData(row, "provider");
-
- return ;
- },
- enableSorting: false,
- },
- // Service column
- {
- accessorKey: "service",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const { servicename } = getFindingsMetadata(row);
- return (
-
- {servicename}
-
- );
- },
- enableSorting: false,
- },
- // Region column
- {
- accessorKey: "region",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const region = getResourceData(row, "region");
- const regionText = typeof region === "string" ? region : "-";
- return (
-
- {regionText}
-
- );
- },
- enableSorting: false,
- },
- // TODO: PROWLER-379 - Enable Impacted Resources column when backend supports grouped findings
- // {
- // accessorKey: "impactedResources",
- // header: ({ column }) => (
- //
- // ),
- // cell: () => {
- // return ;
- // },
- // enableSorting: false,
- // },
- // Time column
- {
- accessorKey: "updated_at",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const {
- attributes: { updated_at },
- } = getFindingsData(row);
- return ;
- },
- },
- // Actions column - dropdown with Mute/Jira options
- {
- id: "actions",
- header: () =>
,
- cell: ({ row }) => ,
- enableSorting: false,
- },
- ];
-}
diff --git a/ui/components/findings/table/column-standalone-findings.tsx b/ui/components/findings/table/column-standalone-findings.tsx
new file mode 100644
index 0000000000..cdd924a0b0
--- /dev/null
+++ b/ui/components/findings/table/column-standalone-findings.tsx
@@ -0,0 +1,245 @@
+"use client";
+
+import { ColumnDef } from "@tanstack/react-table";
+import { Container } from "lucide-react";
+
+import { DateWithTime, EntityInfo } from "@/components/ui/entities";
+import {
+ DataTableColumnHeader,
+ SeverityBadge,
+ StatusFindingBadge,
+} from "@/components/ui/table";
+import { getRegionFlag } from "@/lib/region-flags";
+import { FindingProps, ProviderType } from "@/types";
+
+import { FindingDetailDrawer } from "./finding-detail-drawer";
+import { DeltaValues, NotificationIndicator } from "./notification-indicator";
+import { ProviderIconCell } from "./provider-icon-cell";
+
+interface GetStandaloneFindingColumnsOptions {
+ includeUpdatedAt?: boolean;
+ openFindingId?: string | null;
+}
+
+const getFindingsData = (row: { original: FindingProps }) => {
+ return row.original;
+};
+
+const getFindingsMetadata = (row: { original: FindingProps }) => {
+ return row.original.attributes.check_metadata;
+};
+
+const getResourceData = (
+ row: { original: FindingProps },
+ field: keyof FindingProps["relationships"]["resource"]["attributes"],
+) => {
+ return row.original.relationships?.resource?.attributes?.[field] || "-";
+};
+
+const getProviderData = (
+ row: { original: FindingProps },
+ field: keyof FindingProps["relationships"]["provider"]["attributes"],
+) => {
+ return row.original.relationships?.provider?.attributes?.[field] || "-";
+};
+
+function FindingTitleCell({
+ finding,
+ defaultOpen = false,
+}: {
+ finding: FindingProps;
+ defaultOpen?: boolean;
+}) {
+ return (
+
+
+ {finding.attributes.check_metadata.checktitle}
+
+
+ }
+ />
+ );
+}
+
+export function getStandaloneFindingColumns({
+ includeUpdatedAt = false,
+ openFindingId = null,
+}: GetStandaloneFindingColumnsOptions = {}): ColumnDef[] {
+ const columns: ColumnDef[] = [
+ {
+ id: "notification",
+ header: () => null,
+ cell: ({ row }) => {
+ const finding = row.original;
+ const delta = finding.attributes.delta as
+ | (typeof DeltaValues)[keyof typeof DeltaValues]
+ | undefined;
+
+ return (
+
+ );
+ },
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: "status",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { status },
+ } = getFindingsData(row);
+
+ return ;
+ },
+ },
+ {
+ accessorKey: "check",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: "resourceName",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const name = getResourceData(row, "name");
+ const uid = getResourceData(row, "uid");
+ const entityAlias =
+ typeof name === "string" && name.trim().length > 0 && name !== "-"
+ ? name
+ : undefined;
+ const entityId =
+ typeof uid === "string" && uid.trim().length > 0 && uid !== "-"
+ ? uid
+ : undefined;
+
+ return (
+
+ }
+ entityAlias={entityAlias}
+ entityId={entityId}
+ />
+
+ );
+ },
+ enableSorting: false,
+ },
+ {
+ accessorKey: "severity",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { severity },
+ } = getFindingsData(row);
+ return ;
+ },
+ },
+ {
+ accessorKey: "provider",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const provider = getProviderData(row, "provider");
+
+ return (
+
+ );
+ },
+ enableSorting: false,
+ },
+ {
+ accessorKey: "service",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const { servicename } = getFindingsMetadata(row);
+ return (
+
+ {servicename}
+
+ );
+ },
+ enableSorting: false,
+ },
+ {
+ accessorKey: "region",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const region = getResourceData(row, "region");
+ const regionText = typeof region === "string" ? region : "-";
+ const regionFlag =
+ typeof region === "string" ? getRegionFlag(region) : "";
+ return (
+
+ {regionFlag && (
+
+ {regionFlag}
+
+ )}
+ {regionText}
+
+ );
+ },
+ enableSorting: false,
+ },
+ ];
+
+ if (includeUpdatedAt) {
+ columns.push({
+ accessorKey: "updated_at",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { updated_at },
+ } = getFindingsData(row);
+ return ;
+ },
+ });
+ }
+
+ return columns;
+}
diff --git a/ui/components/findings/table/data-table-row-details.tsx b/ui/components/findings/table/data-table-row-details.tsx
deleted file mode 100644
index 54f0f96eeb..0000000000
--- a/ui/components/findings/table/data-table-row-details.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-"use client";
-
-import { FindingProps } from "@/types/components";
-
-import { FindingDetail } from "./finding-detail";
-
-export const DataTableRowDetails = ({
- findingDetails,
-}: {
- entityId: string;
- findingDetails: FindingProps;
-}) => {
- return ;
-};
diff --git a/ui/components/findings/table/finding-detail-drawer.test.ts b/ui/components/findings/table/finding-detail-drawer.test.ts
new file mode 100644
index 0000000000..4054c67f1d
--- /dev/null
+++ b/ui/components/findings/table/finding-detail-drawer.test.ts
@@ -0,0 +1,22 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("finding detail drawer", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "finding-detail-drawer.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("uses the shared resource detail drawer hook with single-resource mode", () => {
+ expect(source).toContain("useResourceDetailDrawer");
+ expect(source).toContain("totalResourceCount: 1");
+ expect(source).toContain("initialIndex: defaultOpen || inline ? 0 : null");
+ });
+
+ it("renders the new resource detail drawer content instead of the legacy finding detail component", () => {
+ expect(source).toContain("ResourceDetailDrawerContent");
+ expect(source).not.toContain('from "./finding-detail"');
+ });
+});
diff --git a/ui/components/findings/table/finding-detail-drawer.tsx b/ui/components/findings/table/finding-detail-drawer.tsx
new file mode 100644
index 0000000000..93f8dd0438
--- /dev/null
+++ b/ui/components/findings/table/finding-detail-drawer.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import type { ReactNode } from "react";
+
+import { findingToFindingResourceRow } from "@/lib/finding-detail";
+import type { FindingProps } from "@/types/components";
+
+import {
+ ResourceDetailDrawer,
+ useResourceDetailDrawer,
+} from "./resource-detail-drawer";
+import { ResourceDetailDrawerContent } from "./resource-detail-drawer/resource-detail-drawer-content";
+
+interface FindingDetailDrawerProps {
+ finding: FindingProps;
+ trigger?: ReactNode;
+ defaultOpen?: boolean;
+ inline?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ onMuteComplete?: () => void;
+}
+
+export function FindingDetailDrawer({
+ finding,
+ trigger,
+ defaultOpen = false,
+ inline = false,
+ onOpenChange,
+ onMuteComplete,
+}: FindingDetailDrawerProps) {
+ const drawer = useResourceDetailDrawer({
+ resources: [findingToFindingResourceRow(finding)],
+ totalResourceCount: 1,
+ initialIndex: defaultOpen || inline ? 0 : null,
+ });
+
+ const handleOpen = () => {
+ drawer.openDrawer(0);
+ onOpenChange?.(true);
+ };
+
+ const handleOpenChange = (open: boolean) => {
+ if (open) {
+ drawer.openDrawer(0);
+ } else {
+ drawer.closeDrawer();
+ }
+
+ onOpenChange?.(open);
+ };
+
+ const handleMuteComplete = () => {
+ drawer.refetchCurrent();
+ onMuteComplete?.();
+ };
+
+ if (inline) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+ {trigger ? (
+
+ {trigger}
+
+ ) : null}
+
+ >
+ );
+}
diff --git a/ui/components/findings/table/finding-detail.test.tsx b/ui/components/findings/table/finding-detail.test.tsx
deleted file mode 100644
index ec2e84e2fb..0000000000
--- a/ui/components/findings/table/finding-detail.test.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { describe, expect, it, vi } from "vitest";
-
-import { FindingProps } from "@/types";
-
-import { FindingDetail } from "./finding-detail";
-
-// Mock next/navigation
-const mockRefresh = vi.fn();
-vi.mock("next/navigation", () => ({
- useRouter: () => ({ refresh: mockRefresh }),
- usePathname: () => "/findings",
- useSearchParams: () => new URLSearchParams(),
-}));
-
-// Mock @/components/shadcn to avoid next-auth import chain
-vi.mock("@/components/shadcn", () => {
- const Slot = ({ children }: { children: React.ReactNode }) => <>{children}>;
- return {
- Button: ({
- children,
- ...props
- }: React.ButtonHTMLAttributes & {
- variant?: string;
- size?: string;
- }) => {children} ,
- Drawer: ({ children }: { children: React.ReactNode }) => <>{children}>,
- DrawerClose: ({ children }: { children: React.ReactNode }) => (
- <>{children}>
- ),
- DrawerContent: ({ children }: { children: React.ReactNode }) => (
- <>{children}>
- ),
- DrawerDescription: ({ children }: { children: React.ReactNode }) => (
- <>{children}>
- ),
- DrawerHeader: ({ children }: { children: React.ReactNode }) => (
- <>{children}>
- ),
- DrawerTitle: ({ children }: { children: React.ReactNode }) => (
- <>{children}>
- ),
- DrawerTrigger: Slot,
- InfoField: ({
- children,
- label,
- }: {
- children: React.ReactNode;
- label: string;
- variant?: string;
- }) => (
-
- {label}
- {children}
-
- ),
- Tabs: ({ children }: { children: React.ReactNode }) => <>{children}>,
- TabsContent: ({ children }: { children: React.ReactNode }) => (
- <>{children}>
- ),
- TabsList: ({ children }: { children: React.ReactNode }) => <>{children}>,
- TabsTrigger: ({ children }: { children: React.ReactNode }) => (
- <>{children}>
- ),
- Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}>,
- TooltipContent: ({ children }: { children: React.ReactNode }) => (
- <>{children}>
- ),
- TooltipTrigger: Slot,
- };
-});
-
-vi.mock("@/components/ui/code-snippet/code-snippet", () => ({
- CodeSnippet: ({ value }: { value: string }) => {value} ,
-}));
-
-vi.mock("@/components/ui/custom/custom-link", () => ({
- CustomLink: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
-}));
-
-vi.mock("@/components/ui/entities", () => ({
- EntityInfo: () =>
,
-}));
-
-vi.mock("@/components/ui/entities/date-with-time", () => ({
- DateWithTime: ({ dateTime }: { dateTime: string }) => {dateTime} ,
-}));
-
-vi.mock("@/components/ui/table/severity-badge", () => ({
- SeverityBadge: ({ severity }: { severity: string }) => (
- {severity}
- ),
-}));
-
-vi.mock("@/components/ui/table/status-finding-badge", () => ({
- FindingStatus: {},
- StatusFindingBadge: ({ status }: { status: string }) => {status} ,
-}));
-
-vi.mock("@/lib/iac-utils", () => ({
- buildGitFileUrl: () => null,
- extractLineRangeFromUid: () => null,
-}));
-
-vi.mock("@/lib/utils", () => ({
- cn: (...args: string[]) => args.filter(Boolean).join(" "),
-}));
-
-// Mock child components that are not under test
-vi.mock("../mute-findings-modal", () => ({
- MuteFindingsModal: ({
- isOpen,
- findingIds,
- }: {
- isOpen: boolean;
- findingIds: string[];
- }) =>
- isOpen ? (
- Muting {findingIds.length} finding(s)
- ) : null,
-}));
-
-vi.mock("../muted", () => ({
- Muted: ({ isMuted }: { isMuted: boolean }) =>
- isMuted ? Muted : null,
-}));
-
-vi.mock("./delta-indicator", () => ({
- DeltaIndicator: () => null,
-}));
-
-vi.mock("@/components/shared/events-timeline/events-timeline", () => ({
- EventsTimeline: () =>
,
-}));
-
-vi.mock("react-markdown", () => ({
- default: ({ children }: { children: string }) => {children} ,
-}));
-
-const baseFinding: FindingProps = {
- type: "findings",
- id: "finding-123",
- attributes: {
- uid: "uid-123",
- delta: null,
- status: "FAIL",
- status_extended: "S3 bucket is publicly accessible",
- severity: "high",
- check_id: "s3_bucket_public_access",
- muted: false,
- check_metadata: {
- risk: "Public access risk",
- notes: "",
- checkid: "s3_bucket_public_access",
- provider: "aws",
- severity: "high",
- checktype: [],
- dependson: [],
- relatedto: [],
- categories: ["security"],
- checktitle: "S3 Bucket Public Access Check",
- compliance: null,
- relatedurl: "",
- description: "Checks if S3 buckets are publicly accessible",
- remediation: {
- code: { cli: "", other: "", nativeiac: "", terraform: "" },
- recommendation: { url: "", text: "" },
- },
- servicename: "s3",
- checkaliases: [],
- resourcetype: "AwsS3Bucket",
- subservicename: "",
- resourceidtemplate: "",
- },
- raw_result: null,
- inserted_at: "2024-01-01T00:00:00Z",
- updated_at: "2024-01-02T00:00:00Z",
- first_seen_at: "2024-01-01T00:00:00Z",
- },
- relationships: {
- resources: { data: [{ type: "resources", id: "res-1" }] },
- scan: {
- data: { type: "scans", id: "scan-1" },
- attributes: {
- name: "Daily Scan",
- trigger: "scheduled",
- state: "completed",
- unique_resource_count: 50,
- progress: 100,
- scanner_args: { checks_to_execute: [] },
- duration: 120,
- started_at: "2024-01-01T00:00:00Z",
- inserted_at: "2024-01-01T00:00:00Z",
- completed_at: "2024-01-01T00:02:00Z",
- scheduled_at: null,
- next_scan_at: "2024-01-02T00:00:00Z",
- },
- },
- resource: {
- data: [{ type: "resources", id: "res-1" }],
- id: "res-1",
- attributes: {
- uid: "arn:aws:s3:::my-bucket",
- name: "my-bucket",
- region: "us-east-1",
- service: "s3",
- tags: {},
- type: "AwsS3Bucket",
- inserted_at: "2024-01-01T00:00:00Z",
- updated_at: "2024-01-01T00:00:00Z",
- details: null,
- partition: "aws",
- },
- relationships: {
- provider: { data: { type: "providers", id: "prov-1" } },
- findings: {
- meta: { count: 1 },
- data: [{ type: "findings", id: "finding-123" }],
- },
- },
- links: { self: "/resources/res-1" },
- },
- provider: {
- data: { type: "providers", id: "prov-1" },
- attributes: {
- provider: "aws",
- uid: "123456789012",
- alias: "my-account",
- connection: {
- connected: true,
- last_checked_at: "2024-01-01T00:00:00Z",
- },
- inserted_at: "2024-01-01T00:00:00Z",
- updated_at: "2024-01-01T00:00:00Z",
- },
- relationships: {
- secret: { data: { type: "provider-secrets", id: "secret-1" } },
- },
- links: { self: "/providers/prov-1" },
- },
- },
- links: { self: "/findings/finding-123" },
-};
-
-describe("FindingDetail", () => {
- it("shows the Mute button for non-muted findings", () => {
- render( );
-
- expect(screen.getByRole("button", { name: /mute/i })).toBeInTheDocument();
- });
-
- it("hides the Mute button for muted findings", () => {
- const mutedFinding: FindingProps = {
- ...baseFinding,
- attributes: { ...baseFinding.attributes, muted: true },
- };
-
- render( );
-
- expect(screen.queryByRole("button", { name: /mute/i })).toBeNull();
- });
-
- it("opens the mute modal when clicking the Mute button", async () => {
- const user = userEvent.setup();
-
- render( );
-
- expect(screen.queryByTestId("mute-modal")).toBeNull();
-
- await user.click(screen.getByRole("button", { name: /mute/i }));
-
- expect(screen.getByTestId("mute-modal")).toBeInTheDocument();
- });
-
- it("does not render the mute modal for muted findings", () => {
- const mutedFinding: FindingProps = {
- ...baseFinding,
- attributes: { ...baseFinding.attributes, muted: true },
- };
-
- render( );
-
- expect(screen.queryByTestId("mute-modal")).toBeNull();
- });
-
- it("shows the muted badge for muted findings", () => {
- const mutedFinding: FindingProps = {
- ...baseFinding,
- attributes: { ...baseFinding.attributes, muted: true },
- };
-
- render( );
-
- expect(screen.getByTestId("muted-badge")).toBeInTheDocument();
- });
-});
diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx
deleted file mode 100644
index 1b95a6653b..0000000000
--- a/ui/components/findings/table/finding-detail.tsx
+++ /dev/null
@@ -1,502 +0,0 @@
-// TODO: Legacy component — used by /resources page and overview dashboard.
-// Migrate those consumers to the new resource-detail-drawer, then delete this file.
-"use client";
-
-import { ExternalLink, Link, VolumeX, X } from "lucide-react";
-import { usePathname, useRouter, useSearchParams } from "next/navigation";
-import { type ReactNode, useState } from "react";
-
-import {
- Button,
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
- InfoField,
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/shadcn";
-import { EventsTimeline } from "@/components/shared/events-timeline/events-timeline";
-import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
-import { CustomLink } from "@/components/ui/custom/custom-link";
-import { EntityInfo } from "@/components/ui/entities";
-import { DateWithTime } from "@/components/ui/entities/date-with-time";
-import { SeverityBadge } from "@/components/ui/table/severity-badge";
-import {
- FindingStatus,
- StatusFindingBadge,
-} from "@/components/ui/table/status-finding-badge";
-import { formatDuration } from "@/lib/date-utils";
-import { buildGitFileUrl, extractLineRangeFromUid } from "@/lib/iac-utils";
-import { cn } from "@/lib/utils";
-import { FindingProps, ProviderType } from "@/types";
-
-import { MarkdownContainer } from "../markdown-container";
-import { MuteFindingsModal } from "../mute-findings-modal";
-import { Muted } from "../muted";
-import { DeltaIndicator } from "./delta-indicator";
-
-const renderValue = (value: string | null | undefined) => {
- return value && value.trim() !== "" ? value : "-";
-};
-
-interface FindingDetailProps {
- findingDetails: FindingProps;
- trigger?: ReactNode;
- open?: boolean;
- defaultOpen?: boolean;
- onOpenChange?: (open: boolean) => void;
-}
-
-export const FindingDetail = ({
- findingDetails,
- trigger,
- open,
- defaultOpen = false,
- onOpenChange,
-}: FindingDetailProps) => {
- const finding = findingDetails;
- const attributes = finding.attributes;
- const resource = finding.relationships?.resource?.attributes;
- const scan = finding.relationships?.scan?.attributes;
- const providerDetails = finding.relationships?.provider?.attributes;
- const router = useRouter();
- const pathname = usePathname();
- const searchParams = useSearchParams();
- const [isMuteModalOpen, setIsMuteModalOpen] = useState(false);
-
- const copyFindingUrl = () => {
- const params = new URLSearchParams(searchParams.toString());
- params.set("id", findingDetails.id);
- const url = `${window.location.origin}${pathname}?${params.toString()}`;
- navigator.clipboard.writeText(url);
- };
-
- // Build Git URL for IaC findings
- const gitUrl =
- providerDetails?.provider === "iac" && resource
- ? buildGitFileUrl(
- providerDetails.uid,
- resource.name,
- extractLineRangeFromUid(attributes.uid) || "",
- resource.region,
- )
- : null;
-
- const handleMuteComplete = () => {
- setIsMuteModalOpen(false);
- onOpenChange?.(false);
- router.refresh();
- };
-
- const muteModal = !attributes.muted && (
-
- );
-
- const content = (
-
- {/* Header */}
-
- {/* Row 1: Status badges */}
-
-
-
- {attributes.delta && (
-
-
-
- {attributes.delta}
-
-
- )}
-
-
-
- {/* Row 2: Title with copy link */}
-
- {renderValue(attributes.check_metadata.checktitle)}
-
-
-
-
-
-
- Copy finding link to clipboard
-
-
-
- {/* Row 3: First Seen */}
-
- Time:
-
-
-
-
- {/* Tabs */}
-
-
-
- General
- Resources
- Scans
- Events
-
-
- {!attributes.muted && (
- setIsMuteModalOpen(true)}
- >
-
- Mute
-
- )}
-
-
- {/* General Tab */}
-
-
- Here is an overview of this finding:
-
-
- {providerDetails && (
-
- )}
-
- {attributes.check_metadata.servicename}
-
- {resource?.region ?? "-"}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {attributes.status === "FAIL" && (
-
-
-
- {attributes.check_metadata.risk}
-
-
-
- )}
-
-
-
- {attributes.check_metadata.description}
-
-
-
-
- {renderValue(attributes.status_extended)}
-
-
- {attributes.check_metadata.remediation && (
-
-
- Remediation Details
-
-
- {/* Recommendation section */}
- {attributes.check_metadata.remediation.recommendation.text && (
-
-
-
- {
- attributes.check_metadata.remediation.recommendation
- .text
- }
-
-
- {attributes.check_metadata.remediation.recommendation
- .url && (
-
- Learn more
-
- )}
-
-
- )}
-
- {/* CLI Command section */}
- {attributes.check_metadata.remediation.code.cli && (
-
-
-
- {attributes.check_metadata.remediation.code.cli}
-
-
-
- )}
-
- {/* Remediation Steps section */}
- {attributes.check_metadata.remediation.code.other && (
-
-
- {attributes.check_metadata.remediation.code.other}
-
-
- )}
-
- {/* Additional URLs section */}
- {attributes.check_metadata.additionalurls &&
- attributes.check_metadata.additionalurls.length > 0 && (
-
-
- {attributes.check_metadata.additionalurls.map(
- (link, idx) => (
-
-
- {link}
-
-
- ),
- )}
-
-
- )}
-
- )}
-
-
- {attributes.check_metadata.categories?.join(", ") || "none"}
-
-
-
- {/* Resources Tab */}
-
- {resource ? (
- <>
- {providerDetails?.provider === "iac" && gitUrl && (
-
- )}
-
-
-
- {renderValue(resource.name)}
-
-
- {renderValue(resource.type)}
-
-
-
-
-
- {renderValue(resource.service)}
-
-
- {renderValue(resource.region)}
-
-
-
-
-
- {renderValue(resource.partition)}
-
-
- {renderValue(resource.details)}
-
-
-
-
-
-
-
- {resource.tags && Object.entries(resource.tags).length > 0 && (
-
-
- Tags
-
-
- {Object.entries(resource.tags).map(([key, value]) => (
-
- {renderValue(value)}
-
- ))}
-
-
- )}
-
-
-
-
-
-
-
-
-
- >
- ) : (
-
- Resource information is not available.
-
- )}
-
-
- {/* Scans Tab */}
-
- {scan ? (
- <>
-
- {scan.name || "N/A"}
-
- {scan.unique_resource_count}
-
- {scan.progress}%
-
-
-
- {scan.trigger}
- {scan.state}
-
- {formatDuration(scan.duration)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {scan.scheduled_at && (
-
-
-
- )}
-
- >
- ) : (
-
- Scan information is not available.
-
- )}
-
-
- {/* Events Tab */}
-
-
-
-
-
- );
-
- // If no trigger, render content directly (inline mode)
- if (!trigger) {
- return (
- <>
- {muteModal}
- {content}
- >
- );
- }
-
- // With trigger, wrap in Drawer — modal rendered outside to avoid nested overlay issues
- return (
- <>
- {muteModal}
-
- {trigger}
-
-
- Finding Details
- View the finding details
-
-
-
- Close
-
- {content}
-
-
- >
- );
-};
diff --git a/ui/components/findings/table/findings-group-drill-down.test.ts b/ui/components/findings/table/findings-group-drill-down.test.ts
new file mode 100644
index 0000000000..816e894e0a
--- /dev/null
+++ b/ui/components/findings/table/findings-group-drill-down.test.ts
@@ -0,0 +1,16 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("findings group drill down", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "findings-group-drill-down.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("uses the shared finding-group resource state hook", () => {
+ expect(source).toContain("useFindingGroupResourceState");
+ expect(source).not.toContain("useInfiniteResources");
+ });
+});
diff --git a/ui/components/findings/table/findings-group-drill-down.tsx b/ui/components/findings/table/findings-group-drill-down.tsx
index 3d35b20115..2bee7d5667 100644
--- a/ui/components/findings/table/findings-group-drill-down.tsx
+++ b/ui/components/findings/table/findings-group-drill-down.tsx
@@ -3,15 +3,12 @@
import {
flexRender,
getCoreRowModel,
- Row,
- RowSelectionState,
useReactTable,
} from "@tanstack/react-table";
import { ChevronLeft } from "lucide-react";
import { useSearchParams } from "next/navigation";
-import { useState } from "react";
-import { Spinner } from "@/components/shadcn/spinner/spinner";
+import { LoadingState } from "@/components/shadcn/spinner/loading-state";
import {
Table,
TableBody,
@@ -21,24 +18,22 @@ import {
TableRow,
} from "@/components/ui/table";
import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table";
-import { useInfiniteResources } from "@/hooks/use-infinite-resources";
+import { useFindingGroupResourceState } from "@/hooks/use-finding-group-resource-state";
import { cn, hasHistoricalFindingFilter } from "@/lib";
import {
getFilteredFindingGroupDelta,
+ getFindingGroupImpactedCounts,
isFindingGroupMuted,
} from "@/lib/findings-groups";
-import { FindingGroupRow, FindingResourceRow } from "@/types";
+import { FindingGroupRow } from "@/types";
import { FloatingMuteButton } from "../floating-mute-button";
import { getColumnFindingResources } from "./column-finding-resources";
-import { canMuteFindingResource } from "./finding-resource-selection";
import { FindingsSelectionContext } from "./findings-selection-context";
import { ImpactedResourcesCell } from "./impacted-resources-cell";
-import { DeltaValues, NotificationIndicator } from "./notification-indicator";
-import {
- ResourceDetailDrawer,
- useResourceDetailDrawer,
-} from "./resource-detail-drawer";
+import { getFindingGroupEmptyStateMessage } from "./inline-resource-container.utils";
+import { NotificationIndicator } from "./notification-indicator";
+import { ResourceDetailDrawer } from "./resource-detail-drawer";
interface FindingsGroupDrillDownProps {
group: FindingGroupRow;
@@ -50,9 +45,6 @@ export function FindingsGroupDrillDown({
onCollapse,
}: FindingsGroupDrillDownProps) {
const searchParams = useSearchParams();
- const [rowSelection, setRowSelection] = useState({});
- const [resources, setResources] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
// Keep drill-down endpoint selection aligned with the grouped findings page.
const currentParams = Object.fromEntries(searchParams.entries());
@@ -66,78 +58,27 @@ export function FindingsGroupDrillDown({
}
});
- const handleSetResources = (
- newResources: FindingResourceRow[],
- _hasMore: boolean,
- ) => {
- setResources(newResources);
- setIsLoading(false);
- };
-
- const handleAppendResources = (
- newResources: FindingResourceRow[],
- _hasMore: boolean,
- ) => {
- setResources((prev) => [...prev, ...newResources]);
- setIsLoading(false);
- };
-
- const handleSetLoading = (loading: boolean) => {
- setIsLoading(loading);
- };
-
- const { sentinelRef, refresh, loadMore, totalCount } = useInfiniteResources({
- checkId: group.checkId,
- hasDateOrScanFilter: hasHistoricalFilterActive,
- filters,
- onSetResources: handleSetResources,
- onAppendResources: handleAppendResources,
- onSetLoading: handleSetLoading,
- });
-
- // Resource detail drawer
- const drawer = useResourceDetailDrawer({
+ const {
+ rowSelection,
resources,
- checkId: group.checkId,
- totalResourceCount: totalCount ?? group.resourcesTotal,
- onRequestMoreResources: loadMore,
+ isLoading,
+ sentinelRef,
+ drawer,
+ handleDrawerMuteComplete,
+ selectedFindingIds,
+ selectableRowCount,
+ getRowCanSelect,
+ clearSelection,
+ isSelected,
+ handleMuteComplete,
+ handleRowSelectionChange,
+ resolveSelectedFindingIds,
+ } = useFindingGroupResourceState({
+ group,
+ filters,
+ hasHistoricalData: hasHistoricalFilterActive,
});
- const handleDrawerMuteComplete = () => {
- drawer.refetchCurrent();
- refresh();
- };
-
- // Selection logic — tracks by findingId (resource_id) for checkbox consistency
- const selectedFindingIds = Object.keys(rowSelection)
- .filter((key) => rowSelection[key])
- .map((idx) => resources[parseInt(idx)]?.findingId)
- .filter((id): id is string => id !== null && id !== undefined && id !== "");
-
- /** findingId values are already real finding UUIDs — no resolution needed. */
- const resolveResourceIds = async (ids: string[]) => {
- return ids.filter(Boolean);
- };
-
- const selectableRowCount = resources.filter(canMuteFindingResource).length;
-
- const getRowCanSelect = (row: Row): boolean => {
- return canMuteFindingResource(row.original);
- };
-
- const clearSelection = () => {
- setRowSelection({});
- };
-
- const isSelected = (id: string) => {
- return selectedFindingIds.includes(id);
- };
-
- const handleMuteComplete = () => {
- clearSelection();
- refresh();
- };
-
const columns = getColumnFindingResources({
rowSelection,
selectableRowCount,
@@ -148,7 +89,7 @@ export function FindingsGroupDrillDown({
columns,
enableRowSelection: getRowCanSelect,
getCoreRowModel: getCoreRowModel(),
- onRowSelectionChange: setRowSelection,
+ onRowSelectionChange: handleRowSelectionChange,
manualPagination: true,
state: {
rowSelection,
@@ -157,14 +98,8 @@ export function FindingsGroupDrillDown({
// Delta for the sticky header
const deltaKey = getFilteredFindingGroupDelta(group, filters);
- const delta =
- deltaKey === "new"
- ? DeltaValues.NEW
- : deltaKey === "changed"
- ? DeltaValues.CHANGED
- : DeltaValues.NONE;
-
const allMuted = isFindingGroupMuted(group);
+ const impactedCounts = getFindingGroupImpactedCounts(group);
const rows = table.getRowModel().rows;
@@ -175,7 +110,7 @@ export function FindingsGroupDrillDown({
selectedFindings: [],
clearSelection,
isSelected,
- resolveMuteIds: resolveResourceIds,
+ resolveMuteIds: resolveSelectedFindingIds,
onMuteComplete: handleMuteComplete,
}}
>
@@ -200,7 +135,7 @@ export function FindingsGroupDrillDown({
{/* Notification indicator */}
@@ -220,8 +155,8 @@ export function FindingsGroupDrillDown({
{/* Impacted resources count */}
@@ -270,9 +205,7 @@ export function FindingsGroupDrillDown({
colSpan={columns.length}
className="h-24 text-center"
>
- {Object.keys(filters).length > 0
- ? "No resources found for the selected filters."
- : "No resources found."}
+ {getFindingGroupEmptyStateMessage(group, filters)}
) : null}
@@ -280,14 +213,7 @@ export function FindingsGroupDrillDown({
{/* Loading indicator */}
- {isLoading && (
-
-
-
- Loading resources...
-
-
- )}
+ {isLoading && }
{/* Sentinel for infinite scroll */}
@@ -299,7 +225,7 @@ export function FindingsGroupDrillDown({
selectedCount={selectedFindingIds.length}
selectedFindingIds={selectedFindingIds}
onBeforeOpen={async () => {
- return resolveResourceIds(selectedFindingIds);
+ return resolveSelectedFindingIds(selectedFindingIds);
}}
onComplete={handleMuteComplete}
isBulkOperation
@@ -316,8 +242,10 @@ export function FindingsGroupDrillDown({
checkMeta={drawer.checkMeta}
currentIndex={drawer.currentIndex}
totalResources={drawer.totalResources}
+ currentResource={drawer.currentResource}
currentFinding={drawer.currentFinding}
otherFindings={drawer.otherFindings}
+ showSyntheticResourceHint={group.resourcesTotal === 0}
onNavigatePrev={drawer.navigatePrev}
onNavigateNext={drawer.navigateNext}
onMuteComplete={handleDrawerMuteComplete}
diff --git a/ui/components/findings/table/findings-group-table.tsx b/ui/components/findings/table/findings-group-table.tsx
index 2f3a515415..1459972e22 100644
--- a/ui/components/findings/table/findings-group-table.tsx
+++ b/ui/components/findings/table/findings-group-table.tsx
@@ -6,6 +6,7 @@ import { useRef, useState } from "react";
import { resolveFindingIdsByVisibleGroupResources } from "@/actions/findings/findings-by-resource";
import { DataTable } from "@/components/ui/table";
+import { canDrillDownFindingGroup } from "@/lib/findings-groups";
import { FindingGroupRow, MetaDataProps } from "@/types";
import { FloatingMuteButton } from "../floating-mute-button";
@@ -140,7 +141,7 @@ export function FindingsGroupTable({
const handleDrillDown = (checkId: string, group: FindingGroupRow) => {
// No resources in the group → nothing to show, skip drill-down
- if (group.resourcesTotal === 0) return;
+ if (!canDrillDownFindingGroup(group)) return;
// Toggle: same group = collapse, different = switch
if (expandedCheckId === checkId) {
diff --git a/ui/components/findings/table/index.ts b/ui/components/findings/table/index.ts
index b3b063a6ed..f8b6b4fdbe 100644
--- a/ui/components/findings/table/index.ts
+++ b/ui/components/findings/table/index.ts
@@ -1,16 +1,11 @@
export * from "./column-finding-groups";
export * from "./column-finding-resources";
-export * from "./column-findings";
+export * from "./column-standalone-findings";
export * from "./data-table-row-actions";
-export * from "./data-table-row-details";
-export * from "./finding-detail";
+export * from "./finding-detail-drawer";
export * from "./findings-group-drill-down";
export * from "./findings-group-table";
export * from "./findings-selection-context";
-// TODO: Remove legacy exports once /resources and overview dashboard migrate to grouped view components
-// export * from "./column-findings";
-// export * from "./data-table-row-details";
-// export * from "./finding-detail";
export * from "./impacted-resources-cell";
export * from "./notification-indicator";
export * from "./provider-icon-cell";
diff --git a/ui/components/findings/table/inline-resource-container.test.ts b/ui/components/findings/table/inline-resource-container.test.ts
new file mode 100644
index 0000000000..3acafa8ee5
--- /dev/null
+++ b/ui/components/findings/table/inline-resource-container.test.ts
@@ -0,0 +1,16 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("inline resource container", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "inline-resource-container.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("uses the shared finding-group resource state hook", () => {
+ expect(source).toContain("useFindingGroupResourceState");
+ expect(source).not.toContain("useInfiniteResources");
+ });
+});
diff --git a/ui/components/findings/table/inline-resource-container.tsx b/ui/components/findings/table/inline-resource-container.tsx
index c2f40e189f..2aa13056d9 100644
--- a/ui/components/findings/table/inline-resource-container.tsx
+++ b/ui/components/findings/table/inline-resource-container.tsx
@@ -3,32 +3,27 @@
import {
flexRender,
getCoreRowModel,
- Row,
- RowSelectionState,
useReactTable,
} from "@tanstack/react-table";
import { AnimatePresence, motion } from "framer-motion";
import { ChevronsDown } from "lucide-react";
-import { useImperativeHandle, useRef, useState } from "react";
+import { useImperativeHandle, useRef } from "react";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
-import { Spinner } from "@/components/shadcn/spinner/spinner";
+import { LoadingState } from "@/components/shadcn/spinner/loading-state";
import { TableCell, TableRow } from "@/components/ui/table";
-import { useInfiniteResources } from "@/hooks/use-infinite-resources";
+import { useFindingGroupResourceState } from "@/hooks/use-finding-group-resource-state";
import { useScrollHint } from "@/hooks/use-scroll-hint";
-import { FindingGroupRow, FindingResourceRow } from "@/types";
+import { FindingGroupRow } from "@/types";
import { getColumnFindingResources } from "./column-finding-resources";
-import { canMuteFindingResource } from "./finding-resource-selection";
import { FindingsSelectionContext } from "./findings-selection-context";
import {
getFilteredFindingGroupResourceCount,
+ getFindingGroupEmptyStateMessage,
getFindingGroupSkeletonCount,
} from "./inline-resource-container.utils";
-import {
- ResourceDetailDrawer,
- useResourceDetailDrawer,
-} from "./resource-detail-drawer";
+import { ResourceDetailDrawer } from "./resource-detail-drawer";
export interface InlineResourceContainerHandle {
/** Soft-refresh resources (re-fetch page 1 without skeletons). */
@@ -140,22 +135,6 @@ export function InlineResourceContainer({
ref,
}: InlineResourceContainerProps) {
const scrollContainerRef = useRef(null);
- const [rowSelection, setRowSelection] = useState({});
- const [resources, setResources] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
- // Scroll hint: shows "scroll for more" when content overflows
- const {
- containerRef: scrollHintContainerRef,
- sentinelRef: scrollHintSentinelRef,
- showScrollHint,
- } = useScrollHint({ refreshToken: resources.length });
-
- // Combine scrollContainerRef (for IntersectionObserver root) with scrollHintContainerRef
- const combinedScrollRef = (node: HTMLDivElement | null) => {
- scrollContainerRef.current = node;
- scrollHintContainerRef(node);
- };
-
const filters: Record = { ...resolvedFilters };
if (resourceSearch) {
filters["filter[name__icontains]"] = resourceSearch;
@@ -171,99 +150,45 @@ export function InlineResourceContainer({
filters,
);
- const handleSetResources = (
- newResources: FindingResourceRow[],
- _hasMore: boolean,
- ) => {
- setResources(newResources);
- setIsLoading(false);
- };
-
- const handleAppendResources = (
- newResources: FindingResourceRow[],
- _hasMore: boolean,
- ) => {
- setResources((prev) => [...prev, ...newResources]);
- setIsLoading(false);
- };
-
- const handleSetLoading = (loading: boolean) => {
- setIsLoading(loading);
- };
-
- const { sentinelRef, refresh, loadMore, totalCount } = useInfiniteResources({
- checkId: group.checkId,
- hasDateOrScanFilter: hasHistoricalData,
+ const {
+ rowSelection,
+ resources,
+ isLoading,
+ sentinelRef,
+ refresh,
+ drawer,
+ handleDrawerMuteComplete,
+ selectedFindingIds,
+ selectableRowCount,
+ getRowCanSelect,
+ clearSelection,
+ isSelected,
+ handleMuteComplete,
+ handleRowSelectionChange,
+ resolveSelectedFindingIds,
+ } = useFindingGroupResourceState({
+ group,
filters,
- onSetResources: handleSetResources,
- onAppendResources: handleAppendResources,
- onSetLoading: handleSetLoading,
+ hasHistoricalData,
+ onResourceSelectionChange,
scrollContainerRef,
});
- // Resource detail drawer
- const drawer = useResourceDetailDrawer({
- resources,
- checkId: group.checkId,
- totalResourceCount: totalCount ?? group.resourcesTotal,
- onRequestMoreResources: loadMore,
- });
+ // Scroll hint: shows "scroll for more" when content overflows
+ const {
+ containerRef: scrollHintContainerRef,
+ sentinelRef: scrollHintSentinelRef,
+ showScrollHint,
+ } = useScrollHint({ refreshToken: resources.length });
- const handleDrawerMuteComplete = () => {
- drawer.refetchCurrent();
- refresh();
- };
-
- // Selection logic
- const selectedFindingIds = Object.keys(rowSelection)
- .filter((key) => rowSelection[key])
- .map((idx) => resources[parseInt(idx)]?.findingId)
- .filter(Boolean);
-
- const resolveResourceIds = async (ids: string[]) => {
- // findingId values are already real finding UUIDs (from the group
- // resources endpoint), so no second resolution round-trip is needed.
- return ids.filter(Boolean);
- };
-
- const selectableRowCount = resources.filter(canMuteFindingResource).length;
-
- const getRowCanSelect = (row: Row): boolean => {
- return canMuteFindingResource(row.original);
- };
-
- const clearSelection = () => {
- setRowSelection({});
- onResourceSelectionChange([]);
+ // Combine scrollContainerRef (for IntersectionObserver root) with scrollHintContainerRef
+ const combinedScrollRef = (node: HTMLDivElement | null) => {
+ scrollContainerRef.current = node;
+ scrollHintContainerRef(node);
};
useImperativeHandle(ref, () => ({ refresh, clearSelection }));
- const isSelected = (id: string) => {
- return selectedFindingIds.includes(id);
- };
-
- const handleMuteComplete = () => {
- clearSelection();
- refresh();
- };
-
- const handleRowSelectionChange = (
- updater:
- | RowSelectionState
- | ((prev: RowSelectionState) => RowSelectionState),
- ) => {
- const newSelection =
- typeof updater === "function" ? updater(rowSelection) : updater;
- setRowSelection(newSelection);
-
- const newFindingIds = Object.keys(newSelection)
- .filter((key) => newSelection[key])
- .map((idx) => resources[parseInt(idx)]?.findingId)
- .filter(Boolean);
- onResourceSelectionChange(newFindingIds);
- };
-
const columns = getColumnFindingResources({
rowSelection,
selectableRowCount,
@@ -290,7 +215,7 @@ export function InlineResourceContainer({
selectedFindings: [],
clearSelection,
isSelected,
- resolveMuteIds: resolveResourceIds,
+ resolveMuteIds: resolveSelectedFindingIds,
onMuteComplete: handleMuteComplete,
}}
>
@@ -354,23 +279,16 @@ export function InlineResourceContainer({
colSpan={columns.length}
className="h-24 text-center"
>
- {Object.keys(filters).length > 0
- ? "No resources found for the selected filters."
- : "No resources found."}
+ {getFindingGroupEmptyStateMessage(group, filters)}
)}
- {/* Spinner for infinite scroll (subsequent pages only) */}
+ {/* Loading state for infinite scroll (subsequent pages only) */}
{isLoading && rows.length > 0 && (
-
-
-
- Loading resources...
-
-
+
)}
{/* Sentinel for scroll hint detection */}
@@ -415,8 +333,10 @@ export function InlineResourceContainer({
checkMeta={drawer.checkMeta}
currentIndex={drawer.currentIndex}
totalResources={drawer.totalResources}
+ currentResource={drawer.currentResource}
currentFinding={drawer.currentFinding}
otherFindings={drawer.otherFindings}
+ showSyntheticResourceHint={group.resourcesTotal === 0}
onNavigatePrev={drawer.navigatePrev}
onNavigateNext={drawer.navigateNext}
onMuteComplete={handleDrawerMuteComplete}
diff --git a/ui/components/findings/table/inline-resource-container.utils.test.ts b/ui/components/findings/table/inline-resource-container.utils.test.ts
index 8fac280669..ac1f968521 100644
--- a/ui/components/findings/table/inline-resource-container.utils.test.ts
+++ b/ui/components/findings/table/inline-resource-container.utils.test.ts
@@ -4,6 +4,7 @@ import type { FindingGroupRow } from "@/types";
import {
getFilteredFindingGroupResourceCount,
+ getFindingGroupEmptyStateMessage,
getFindingGroupSkeletonCount,
isFailOnlyStatusFilter,
} from "./inline-resource-container.utils";
@@ -99,3 +100,47 @@ describe("getFindingGroupSkeletonCount", () => {
).toBe(1);
});
});
+
+describe("getFindingGroupEmptyStateMessage", () => {
+ it("returns the muted hint when muted findings are excluded and no visible resources remain", () => {
+ expect(
+ getFindingGroupEmptyStateMessage(
+ makeGroup({
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ mutedCount: 1,
+ failCount: 0,
+ passCount: 0,
+ }),
+ {
+ "filter[status]": "FAIL",
+ "filter[muted]": "false",
+ },
+ ),
+ ).toBe(
+ "No resources match the current filters. Try enabling Include muted to view muted findings.",
+ );
+ });
+
+ it("keeps the generic filtered empty state when muted findings are already included", () => {
+ expect(
+ getFindingGroupEmptyStateMessage(
+ makeGroup({
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ mutedCount: 1,
+ }),
+ {
+ "filter[status]": "FAIL",
+ "filter[muted]": "include",
+ },
+ ),
+ ).toBe("No resources found for the selected filters.");
+ });
+
+ it("keeps the generic empty state when no filters are active", () => {
+ expect(getFindingGroupEmptyStateMessage(makeGroup(), {})).toBe(
+ "No resources found.",
+ );
+ });
+});
diff --git a/ui/components/findings/table/inline-resource-container.utils.ts b/ui/components/findings/table/inline-resource-container.utils.ts
index 9967ea7da3..274304e03f 100644
--- a/ui/components/findings/table/inline-resource-container.utils.ts
+++ b/ui/components/findings/table/inline-resource-container.utils.ts
@@ -33,6 +33,18 @@ export function isFailOnlyStatusFilter(
return multiStatusValues.length === 1 && multiStatusValues[0] === "FAIL";
}
+function includesMutedFindings(
+ filters: Record,
+): boolean {
+ const mutedFilter = filters["filter[muted]"];
+
+ if (Array.isArray(mutedFilter)) {
+ return mutedFilter.includes("include");
+ }
+
+ return mutedFilter === "include";
+}
+
export function getFilteredFindingGroupResourceCount(
group: FindingGroupRow,
filters: Record,
@@ -53,3 +65,24 @@ export function getFindingGroupSkeletonCount(
// empty state ("No resources found") replaces the skeleton.
return Math.max(1, Math.min(filteredTotal, maxSkeletonRows));
}
+
+export function getFindingGroupEmptyStateMessage(
+ group: FindingGroupRow,
+ filters: Record,
+): string {
+ const hasFilters = Object.keys(filters).length > 0;
+
+ if (!hasFilters) {
+ return "No resources found.";
+ }
+
+ const mutedExcluded = !includesMutedFindings(filters);
+ const hasMutedFindings = (group.mutedCount ?? 0) > 0;
+ const visibleCount = getFilteredFindingGroupResourceCount(group, filters);
+
+ if (mutedExcluded && hasMutedFindings && visibleCount === 0) {
+ return "No resources match the current filters. Try enabling Include muted to view muted findings.";
+ }
+
+ return "No resources found for the selected filters.";
+}
diff --git a/ui/components/findings/table/notification-indicator.tsx b/ui/components/findings/table/notification-indicator.tsx
index 324e216c57..be50ee1515 100644
--- a/ui/components/findings/table/notification-indicator.tsx
+++ b/ui/components/findings/table/notification-indicator.tsx
@@ -17,14 +17,11 @@ import {
} from "@/components/shadcn/tooltip";
import { DOCS_URLS } from "@/lib/external-urls";
import { cn } from "@/lib/utils";
+import { FINDING_DELTA, type FindingDelta } from "@/types";
-export const DeltaValues = {
- NEW: "new",
- CHANGED: "changed",
- NONE: "none",
-} as const;
+export const DeltaValues = FINDING_DELTA;
-export type DeltaType = (typeof DeltaValues)[keyof typeof DeltaValues];
+export type DeltaType = Exclude;
interface NotificationIndicatorProps {
delta?: DeltaType;
@@ -124,12 +121,12 @@ function MutedIndicator({ mutedReason }: { mutedReason?: string }) {
e.stopPropagation()}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
>
-
+
({
mockGetComplianceIcon: vi.fn((_: string) => null as string | null),
mockGetCompliancesOverview: vi.fn(),
mockWindowOpen: vi.fn(),
mockClipboardWriteText: vi.fn(),
mockSearchParamsState: { value: "" },
+ mockNotificationIndicator: vi.fn(),
}));
vi.mock("next/navigation", () => ({
@@ -134,7 +136,12 @@ vi.mock("@/components/shadcn/dropdown", () => ({
}));
vi.mock("@/components/shadcn/skeleton/skeleton", () => ({
- Skeleton: () =>
,
+ Skeleton: ({
+ className,
+ ...props
+ }: HTMLAttributes & { className?: string }) => (
+
+ ),
}));
vi.mock("@/components/shadcn/spinner/spinner", () => ({
@@ -178,16 +185,30 @@ vi.mock("@/components/findings/markdown-container", () => ({
}));
vi.mock("@/components/shared/query-code-editor", () => ({
+ QUERY_EDITOR_LANGUAGE: {
+ OPEN_CYPHER: "openCypher",
+ PLAIN_TEXT: "plainText",
+ SHELL: "shell",
+ HCL: "hcl",
+ BICEP: "bicep",
+ YAML: "yaml",
+ },
QueryCodeEditor: ({
ariaLabel,
+ language,
value,
copyValue,
}: {
ariaLabel: string;
+ language?: string;
value: string;
copyValue?: string;
}) => (
-
+
{ariaLabel}
{value}
({
}));
vi.mock("../notification-indicator", () => ({
- NotificationIndicator: () => null,
+ NotificationIndicator: (props: Record) => {
+ mockNotificationIndicator(props);
+ return null;
+ },
+ DeltaValues: { NEW: "new", CHANGED: "changed", NONE: "none" } as const,
}));
vi.mock("./resource-detail-skeleton", () => ({
@@ -295,6 +320,7 @@ vi.mock("../../muted", () => ({
// ---------------------------------------------------------------------------
import type { ResourceDrawerFinding } from "@/actions/findings";
+import type { FindingResourceRow } from "@/types";
import { ResourceDetailDrawerContent } from "./resource-detail-drawer-content";
import type { CheckMeta } from "./use-resource-detail-drawer";
@@ -360,6 +386,29 @@ const mockFinding: ResourceDrawerFinding = {
scan: null,
};
+const mockResourceRow: FindingResourceRow = {
+ id: "row-1",
+ rowType: "resource",
+ findingId: "finding-1",
+ checkId: "s3_check",
+ providerType: "aws",
+ providerAlias: "prod",
+ providerUid: "123456789",
+ resourceName: "my-bucket",
+ resourceType: "Bucket",
+ resourceGroup: "default",
+ resourceUid: "arn:aws:s3:::bucket",
+ service: "s3",
+ region: "us-east-1",
+ severity: "critical",
+ status: "FAIL",
+ delta: null,
+ isMuted: false,
+ mutedReason: undefined,
+ firstSeenAt: null,
+ lastSeenAt: null,
+};
+
// ---------------------------------------------------------------------------
// Fix 1: Lighthouse AI button text change
// ---------------------------------------------------------------------------
@@ -512,6 +561,34 @@ describe("ResourceDetailDrawerContent — Fix 2: Remediation heading labels", ()
expect(mockClipboardWriteText).toHaveBeenCalledWith("aws s3 ...");
expect(screen.getByText("$ aws s3 ...")).toBeInTheDocument();
});
+
+ it("should pass syntax highlighting languages to each remediation editor", () => {
+ // Given
+ render(
+ ,
+ );
+
+ // When
+ const editors = screen.getAllByTestId("query-code-editor");
+
+ // Then
+ expect(editors[0]).toHaveAttribute("data-language", "shell");
+ expect(editors[1]).toHaveAttribute("data-language", "hcl");
+ expect(editors[2]).toHaveAttribute("data-language", "yaml");
+ expect(editors[0]).toHaveAttribute("data-aria-label", "CLI Command");
+ expect(editors[2]).toHaveAttribute("data-aria-label", "CloudFormation");
+ });
});
// ---------------------------------------------------------------------------
@@ -895,3 +972,461 @@ describe("ResourceDetailDrawerContent — other findings mute refresh", () => {
expect(onMuteComplete).not.toHaveBeenCalled();
});
});
+
+describe("ResourceDetailDrawerContent — synthetic resource empty state", () => {
+ it("should explain that simulated IaC resources never have other findings", () => {
+ // Given/When
+ render(
+ ,
+ );
+
+ // Then
+ expect(
+ screen.getByText(
+ "No other findings are available for this IaC resource.",
+ ),
+ ).toBeInTheDocument();
+ });
+});
+
+describe("ResourceDetailDrawerContent — current resource row display", () => {
+ it("should render resource card fields from the current resource row instead of the fetched finding", () => {
+ // Given
+ const currentResource: FindingResourceRow = {
+ ...mockResourceRow,
+ providerAlias: "row-account",
+ providerUid: "row-provider-uid",
+ resourceName: "row-resource-name",
+ resourceUid: "row-resource-uid",
+ service: "row-service",
+ region: "eu-west-1",
+ resourceType: "row-type",
+ resourceGroup: "row-group",
+ severity: "low",
+ status: "PASS",
+ };
+ const fetchedFinding: ResourceDrawerFinding = {
+ ...mockFinding,
+ providerAlias: "finding-account",
+ providerUid: "finding-provider-uid",
+ resourceName: "finding-resource-name",
+ resourceUid: "finding-resource-uid",
+ resourceService: "finding-service",
+ resourceRegion: "ap-south-1",
+ resourceType: "finding-type",
+ resourceGroup: "finding-group",
+ severity: "critical",
+ status: "FAIL",
+ };
+
+ // When
+ render(
+ ,
+ );
+
+ // Then
+ expect(screen.getByText("row-service")).toBeInTheDocument();
+ expect(screen.getByText("eu-west-1")).toBeInTheDocument();
+ expect(screen.getByText("row-group")).toBeInTheDocument();
+ expect(screen.getByText("row-type")).toBeInTheDocument();
+ expect(screen.getByText("FAIL")).toBeInTheDocument();
+ expect(screen.getByText("critical")).toBeInTheDocument();
+ expect(screen.queryByText("finding-service")).not.toBeInTheDocument();
+ expect(screen.queryByText("ap-south-1")).not.toBeInTheDocument();
+ expect(screen.queryByText("finding-group")).not.toBeInTheDocument();
+ expect(screen.queryByText("finding-type")).not.toBeInTheDocument();
+ });
+
+ it("should prefer the fetched finding status and severity in the header when the current row is stale", () => {
+ // Given
+ const currentResource: FindingResourceRow = {
+ ...mockResourceRow,
+ severity: "critical",
+ status: "FAIL",
+ isMuted: false,
+ };
+ const fetchedFinding: ResourceDrawerFinding = {
+ ...mockFinding,
+ severity: "low",
+ status: "PASS",
+ isMuted: true,
+ mutedReason: "Muted after refresh",
+ };
+
+ // When
+ render(
+ ,
+ );
+
+ // Then
+ expect(screen.getByText("PASS")).toBeInTheDocument();
+ expect(screen.getByText("low")).toBeInTheDocument();
+ expect(screen.queryByText("FAIL")).not.toBeInTheDocument();
+ expect(screen.queryByText("critical")).not.toBeInTheDocument();
+ });
+});
+
+describe("ResourceDetailDrawerContent — header skeleton while navigating", () => {
+ it("should keep row-backed navigation chrome visible while hiding stale finding details during carousel navigation", () => {
+ // Given
+ const currentResource: FindingResourceRow = {
+ ...mockResourceRow,
+ checkId: mockCheckMeta.checkId,
+ resourceName: "next-bucket",
+ resourceUid: "next-resource-uid",
+ service: "ec2",
+ region: "eu-west-1",
+ resourceType: "Instance",
+ resourceGroup: "row-group",
+ severity: "low",
+ status: "PASS",
+ findingId: "finding-2",
+ };
+
+ // When
+ render(
+ ,
+ );
+
+ // Then
+ expect(screen.getByText("PASS")).toBeInTheDocument();
+ expect(screen.getByText("low")).toBeInTheDocument();
+ expect(screen.getByText("ec2")).toBeInTheDocument();
+ expect(screen.getByText("eu-west-1")).toBeInTheDocument();
+ expect(screen.getByText("row-group")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Finding Overview" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Other Findings For This Resource" }),
+ ).toBeInTheDocument();
+ expect(screen.queryByText("uid-1")).not.toBeInTheDocument();
+ expect(screen.queryByText("Status extended")).not.toBeInTheDocument();
+ expect(screen.queryByText("FAIL")).not.toBeInTheDocument();
+ expect(screen.queryByText("critical")).not.toBeInTheDocument();
+ });
+
+ it("should skeletonize stale check-level header content when navigating to a different check", () => {
+ // Given
+ const currentResource: FindingResourceRow = {
+ ...mockResourceRow,
+ checkId: "ec2_check",
+ findingId: "finding-2",
+ severity: "low",
+ status: "PASS",
+ };
+
+ // When
+ render(
+ ,
+ );
+
+ // Then
+ expect(screen.getByTestId("drawer-header-skeleton")).toBeInTheDocument();
+ expect(screen.queryByText("S3 Check")).not.toBeInTheDocument();
+ expect(screen.queryByText("PCI-DSS")).not.toBeInTheDocument();
+ expect(screen.getByText("PASS")).toBeInTheDocument();
+ expect(screen.getByText("low")).toBeInTheDocument();
+ });
+
+ it("should keep same-check overview sections visible while hiding stale finding-specific details during navigation", () => {
+ // Given/When
+ render(
+ ,
+ );
+
+ // Then
+ expect(screen.getByText("Risk:")).toBeInTheDocument();
+ expect(screen.getByText("Description:")).toBeInTheDocument();
+ expect(screen.getByText("Remediation:")).toBeInTheDocument();
+ expect(screen.getByText("security")).toBeInTheDocument();
+ expect(screen.queryByText("Status Extended:")).not.toBeInTheDocument();
+ expect(screen.queryByText("uid-1")).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole("link", {
+ name: "Analyze This Finding With Lighthouse AI",
+ }),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should keep the overview tab shell visible with section skeletons when navigating to a different check", () => {
+ // Given
+ const currentResource: FindingResourceRow = {
+ ...mockResourceRow,
+ checkId: "ec2_check",
+ findingId: "finding-2",
+ severity: "low",
+ status: "PASS",
+ };
+
+ // When
+ render(
+ ,
+ );
+
+ // Then
+ expect(
+ screen.getByTestId("overview-navigation-skeleton"),
+ ).toBeInTheDocument();
+ expect(screen.queryByText("Risk:")).not.toBeInTheDocument();
+ expect(screen.queryByText("Description:")).not.toBeInTheDocument();
+ expect(screen.queryByText("Remediation:")).not.toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Finding Overview" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Other Findings For This Resource" }),
+ ).toBeInTheDocument();
+ });
+
+ it("should keep other findings table headers visible while skeletonizing only the rows during navigation", () => {
+ // Given/When
+ render(
+ ,
+ );
+
+ // Then
+ expect(screen.getByText("Status")).toBeInTheDocument();
+ expect(screen.getByText("Finding")).toBeInTheDocument();
+ expect(screen.getByText("Severity")).toBeInTheDocument();
+ expect(screen.getByText("Time")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("other-findings-total-entries-skeleton"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId("other-findings-navigation-skeleton"),
+ ).toBeInTheDocument();
+ });
+
+ it("should keep scans labels visible while skeletonizing only the scan values during navigation", () => {
+ // Given/When
+ render(
+ ,
+ );
+
+ // Then
+ expect(
+ screen.getByText("Showing the latest scan that evaluated this finding"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("Scan Name")).toBeInTheDocument();
+ expect(screen.getByText("Resources Scanned")).toBeInTheDocument();
+ expect(screen.getByText("Progress")).toBeInTheDocument();
+ expect(screen.getByText("Trigger")).toBeInTheDocument();
+ expect(screen.getByText("State")).toBeInTheDocument();
+ expect(screen.getByText("Duration")).toBeInTheDocument();
+ expect(screen.getByText("Started At")).toBeInTheDocument();
+ expect(screen.getByText("Completed At")).toBeInTheDocument();
+ expect(screen.getByText("Launched At")).toBeInTheDocument();
+ expect(screen.getByText("Scheduled At")).toBeInTheDocument();
+ expect(screen.getByTestId("scans-navigation-skeleton")).toBeInTheDocument();
+ });
+
+ it("should keep the events tab shell visible while showing timeline row skeletons during navigation", () => {
+ // Given/When
+ render(
+ ,
+ );
+
+ // Then
+ expect(screen.getByRole("button", { name: "Events" })).toBeInTheDocument();
+ expect(
+ screen.getByTestId("events-navigation-skeleton"),
+ ).toBeInTheDocument();
+ });
+});
+
+describe("ResourceDetailDrawerContent — other findings delta/muted indicator", () => {
+ const renderWithOtherFinding = (
+ overrides: Partial,
+ ) => {
+ const otherFinding: ResourceDrawerFinding = {
+ ...mockFinding,
+ id: "finding-2",
+ uid: "uid-2",
+ checkId: "ec2_check",
+ checkTitle: "EC2 Check",
+ ...overrides,
+ };
+ render(
+ ,
+ );
+ };
+
+ const lastNotificationIndicatorPropsForOtherRow = () => {
+ const calls = mockNotificationIndicator.mock.calls;
+ // Last call corresponds to the other-finding row (current finding row renders first).
+ return calls[calls.length - 1][0];
+ };
+
+ it("should forward delta='new' to the NotificationIndicator for a new other finding", () => {
+ renderWithOtherFinding({ delta: "new" });
+
+ expect(lastNotificationIndicatorPropsForOtherRow()).toMatchObject({
+ delta: "new",
+ isMuted: false,
+ showDeltaWhenMuted: true,
+ });
+ });
+
+ it("should forward delta='changed' to the NotificationIndicator for a changed other finding", () => {
+ renderWithOtherFinding({ delta: "changed" });
+
+ expect(lastNotificationIndicatorPropsForOtherRow()).toMatchObject({
+ delta: "changed",
+ });
+ });
+
+ it("should pass delta=undefined when the finding has delta='none'", () => {
+ renderWithOtherFinding({ delta: "none" });
+
+ expect(lastNotificationIndicatorPropsForOtherRow()).toMatchObject({
+ delta: undefined,
+ });
+ });
+
+ it("should forward mutedReason and keep delta when a muted other finding is also new", () => {
+ renderWithOtherFinding({
+ delta: "new",
+ isMuted: true,
+ mutedReason: "False positive",
+ });
+
+ expect(lastNotificationIndicatorPropsForOtherRow()).toMatchObject({
+ delta: "new",
+ isMuted: true,
+ mutedReason: "False positive",
+ showDeltaWhenMuted: true,
+ });
+ });
+});
diff --git a/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.tsx b/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.tsx
index ca09719717..6042d5a39f 100644
--- a/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.tsx
+++ b/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.tsx
@@ -37,14 +37,18 @@ import {
ActionDropdownItem,
} from "@/components/shadcn/dropdown";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
-import { Spinner } from "@/components/shadcn/spinner/spinner";
+import { LoadingState } from "@/components/shadcn/spinner/loading-state";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/shadcn/tooltip";
import { EventsTimeline } from "@/components/shared/events-timeline/events-timeline";
-import { QueryCodeEditor } from "@/components/shared/query-code-editor";
+import {
+ QUERY_EDITOR_LANGUAGE,
+ QueryCodeEditor,
+ type QueryEditorLanguage,
+} from "@/components/shared/query-code-editor";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { DateWithTime } from "@/components/ui/entities/date-with-time";
@@ -66,10 +70,11 @@ import { getFailingForLabel } from "@/lib/date-utils";
import { formatDuration } from "@/lib/date-utils";
import { getRegionFlag } from "@/lib/region-flags";
import type { ComplianceOverviewData } from "@/types/compliance";
+import type { FindingResourceRow } from "@/types/findings-table";
import { Muted } from "../../muted";
import { DeltaIndicator } from "../delta-indicator";
-import { NotificationIndicator } from "../notification-indicator";
+import { DeltaValues, NotificationIndicator } from "../notification-indicator";
import { ResourceDetailSkeleton } from "./resource-detail-skeleton";
import type { CheckMeta } from "./use-resource-detail-drawer";
@@ -81,19 +86,49 @@ function stripCodeFences(code: string): string {
.trim();
}
+function resolveNativeIacConfig(providerType: string | undefined): {
+ label: string;
+ language: QueryEditorLanguage;
+} {
+ switch (providerType) {
+ case "aws":
+ return {
+ label: "CloudFormation",
+ language: QUERY_EDITOR_LANGUAGE.YAML,
+ };
+ case "azure":
+ return {
+ label: "Bicep",
+ language: QUERY_EDITOR_LANGUAGE.BICEP,
+ };
+ case "kubernetes":
+ return {
+ label: "Kubernetes Manifest",
+ language: QUERY_EDITOR_LANGUAGE.YAML,
+ };
+ default:
+ return {
+ label: "Native IaC",
+ language: QUERY_EDITOR_LANGUAGE.PLAIN_TEXT,
+ };
+ }
+}
+
function renderRemediationCodeBlock({
label,
value,
copyValue,
+ language = QUERY_EDITOR_LANGUAGE.PLAIN_TEXT,
}: {
label: string;
value: string;
copyValue?: string;
+ language?: QueryEditorLanguage;
}) {
return (
void;
onNavigateNext: () => void;
onMuteComplete: () => void;
@@ -282,8 +319,10 @@ export function ResourceDetailDrawerContent({
checkMeta,
currentIndex,
totalResources,
+ currentResource = null,
currentFinding,
otherFindings,
+ showSyntheticResourceHint = false,
onNavigatePrev,
onNavigateNext,
onMuteComplete,
@@ -337,8 +376,30 @@ export function ResourceDetailDrawerContent({
}
// checkMeta is always available from here.
- // currentFinding may be null during resource loading (e.g. drawer reopen).
- const f = currentFinding;
+ // During carousel navigation we only trust row-backed data until the next
+ // finding payload is fully ready, otherwise stale details flash briefly.
+ const f = isNavigating ? null : currentFinding;
+ const isCheckMetaFresh =
+ !currentResource?.checkId || currentResource.checkId === checkMeta.checkId;
+ const showCheckMetaContent = !isNavigating || isCheckMetaFresh;
+ const findingStatus = f?.status ?? currentResource?.status;
+ const findingSeverity = f?.severity ?? currentResource?.severity;
+ const findingDelta = f?.delta ?? currentResource?.delta;
+ const findingIsMuted = f?.isMuted ?? currentResource?.isMuted;
+ const findingMutedReason =
+ f?.mutedReason ?? currentResource?.mutedReason ?? "This finding is muted";
+ const providerType = currentResource?.providerType ?? f?.providerType;
+ const providerAlias = currentResource?.providerAlias ?? f?.providerAlias;
+ const providerUid = currentResource?.providerUid ?? f?.providerUid;
+ const resourceName = currentResource?.resourceName ?? f?.resourceName;
+ const resourceUid = currentResource?.resourceUid ?? f?.resourceUid;
+ const resourceService = currentResource?.service ?? f?.resourceService;
+ const resourceRegion = currentResource?.region ?? f?.resourceRegion;
+ const resourceGroup = currentResource?.resourceGroup ?? f?.resourceGroup;
+ const resourceType = currentResource?.resourceType ?? f?.resourceType;
+ const resourceRegionLabel = resourceRegion || "-";
+ const firstSeenAt = currentResource?.firstSeenAt ?? f?.firstSeenAt ?? null;
+ const lastSeenAt = currentResource?.lastSeenAt ?? f?.updatedAt ?? null;
const hasPrev = currentIndex > 0;
const hasNext = currentIndex < totalResources - 1;
const selectedScanIds = parseSelectedScanIds(
@@ -351,6 +412,9 @@ export function ResourceDetailDrawerContent({
? (f?.scan?.id ?? null)
: null;
const regionFilter = searchParams.get("filter[region__in]");
+ const nativeIacConfig = resolveNativeIacConfig(providerType);
+ const showOverviewCheckMetaContent = showCheckMetaContent;
+ const showOverviewFindingContent = Boolean(f);
const handleOpenCompliance = async (framework: string) => {
if (!complianceScanId || resolvingFramework) {
@@ -413,104 +477,130 @@ export function ResourceDetailDrawerContent({
/>
)}
- {/* Header: status badges + title (check-level from checkMeta) */}
+ {/* Header: keep row-backed badges visible; only hide stale check metadata */}
- {f &&
}
- {f &&
}
- {f?.delta && (
+ {findingStatus && (
+
+ )}
+ {findingSeverity &&
}
+ {findingDelta && (
-
+
- {f.delta}
+ {findingDelta}
)}
- {f && (
-
+ {findingIsMuted !== undefined && (
+
)}
-
- {checkMeta.checkTitle}
-
+ {showCheckMetaContent ? (
+ <>
+
+ {checkMeta.checkTitle}
+
- {checkMeta.complianceFrameworks.length > 0 && (
-
-
- Compliance Frameworks:
-
-
- {checkMeta.complianceFrameworks.map((framework) => {
- const icon = getComplianceIcon(framework);
- const isNavigable = Boolean(complianceScanId);
- const isResolving = resolvingFramework === framework;
+ {checkMeta.complianceFrameworks.length > 0 && (
+
+
+ Compliance Frameworks:
+
+
+ {checkMeta.complianceFrameworks.map((framework) => {
+ const icon = getComplianceIcon(framework);
+ const isNavigable = Boolean(complianceScanId);
+ const isResolving = resolvingFramework === framework;
- return icon ? (
-
-
- {isNavigable ? (
- void handleOpenCompliance(framework)}
- disabled={Boolean(resolvingFramework)}
- className="flex size-7 shrink-0 items-center justify-center rounded-md border border-gray-300 bg-white p-0.5 transition-shadow hover:shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-wait disabled:opacity-70"
- >
-
- {isResolving && (
- Opening compliance
+ return icon ? (
+
+
+ {isNavigable ? (
+
+ void handleOpenCompliance(framework)
+ }
+ disabled={Boolean(resolvingFramework)}
+ className="flex size-7 shrink-0 items-center justify-center rounded-md border border-gray-300 bg-white p-0.5 transition-shadow hover:shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-wait disabled:opacity-70"
+ >
+
+ {isResolving && (
+
+ Opening compliance
+
+ )}
+
+ ) : (
+
+
+
)}
-
- ) : (
-
-
-
- )}
-
- {framework}
-
- ) : (
-
-
- {isNavigable ? (
- void handleOpenCompliance(framework)}
- disabled={Boolean(resolvingFramework)}
- className="text-text-neutral-secondary inline-flex h-7 shrink-0 items-center rounded-md border border-gray-300 bg-white px-1.5 text-xs transition-shadow hover:shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-wait disabled:opacity-70"
- >
- {framework}
- {isResolving && (
- Opening compliance
+
+ {framework}
+
+ ) : (
+
+
+ {isNavigable ? (
+
+ void handleOpenCompliance(framework)
+ }
+ disabled={Boolean(resolvingFramework)}
+ className="text-text-neutral-secondary inline-flex h-7 shrink-0 items-center rounded-md border border-gray-300 bg-white px-1.5 text-xs transition-shadow hover:shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-wait disabled:opacity-70"
+ >
+ {framework}
+ {isResolving && (
+
+ Opening compliance
+
+ )}
+
+ ) : (
+
+ {framework}
+
)}
-
- ) : (
-
- {framework}
-
- )}
-
- {framework}
-
- );
- })}
+
+
{framework}
+
+ );
+ })}
+
+
+ )}
+ >
+ ) : (
+
)}
@@ -549,7 +639,7 @@ export function ResourceDetailDrawerContent({
{/* Resource card */}
{/* Resource info — shows loading when currentFinding is not yet available */}
- {!f || isNavigating ? (
+ {!currentResource && !f ? (
) : (
<>
@@ -558,95 +648,110 @@ export function ResourceDetailDrawerContent({
{/* Row 1: Account, Resource, Service, Region */}
}
- entityAlias={f.providerAlias}
- entityId={f.providerUid}
+ entityAlias={providerAlias}
+ entityId={providerUid}
/>
}
- entityAlias={f.resourceName}
- entityId={f.resourceUid}
+ entityAlias={resourceName}
+ entityId={resourceUid}
idLabel="UID"
/>
- {f.resourceService}
+ {resourceService}
- {getRegionFlag(f.resourceRegion) && (
+ {getRegionFlag(resourceRegionLabel) && (
- {getRegionFlag(f.resourceRegion)}
+ {getRegionFlag(resourceRegionLabel)}
)}
- {f.resourceRegion}
+ {resourceRegionLabel}
{/* Row 2: Dates */}
-
+
-
+
- {getFailingForLabel(f.firstSeenAt) || "-"}
+ {getFailingForLabel(firstSeenAt) || "-"}
- {f.resourceGroup || "-"}
+ {resourceGroup || "-"}
{/* Row 3: IDs */}
-
+ {currentResource?.findingId || f?.id ? (
+
+ ) : (
+
+ )}
-
+ {f?.uid ? (
+
+ ) : (
+
+ )}
{/* Row 4: Resource metadata */}
- {f.resourceType || "-"}
+ {resourceType || "-"}
{/* Actions button — fixed size, aligned with row 1 */}
-
-
- ) : (
-
- )
- }
- label={f.isMuted ? "Muted" : "Mute"}
- disabled={f.isMuted}
- onSelect={() => setIsMuteModalOpen(true)}
- />
- }
- label="Send to Jira"
- onSelect={() => setIsJiraModalOpen(true)}
- />
-
+ {f ? (
+
+
+ ) : (
+
+ )
+ }
+ label={f.isMuted ? "Muted" : "Mute"}
+ disabled={f.isMuted}
+ onSelect={() => setIsMuteModalOpen(true)}
+ />
+ }
+ label="Send to Jira"
+ onSelect={() => setIsJiraModalOpen(true)}
+ />
+
+ ) : (
+
+ )}
>
@@ -673,159 +778,170 @@ export function ResourceDetailDrawerContent({
value="overview"
className="minimal-scrollbar flex flex-col gap-4 overflow-y-auto"
>
- {/* Card 1: Risk + Description + Status Extended */}
- {(checkMeta.risk || checkMeta.description || f?.statusExtended) && (
-
- {checkMeta.risk && (
-
-
- Risk:
-
- {checkMeta.risk}
-
- )}
- {checkMeta.description && (
-
-
- Description:
-
-
- {checkMeta.description}
-
-
- )}
- {f?.statusExtended && (
-
-
- Status Extended:
-
-
- {f.statusExtended}
-
-
- )}
-
- )}
-
- {/* Card 2: Remediation + Commands */}
- {(checkMeta.remediation.recommendation.text ||
- checkMeta.remediation.code.cli ||
- checkMeta.remediation.code.terraform ||
- checkMeta.remediation.code.nativeiac) && (
-
- {checkMeta.remediation.recommendation.text && (
-
-
- Remediation:
-
-
-
+ {showOverviewCheckMetaContent ? (
+ <>
+ {/* Card 1: Risk + Description + Status Extended */}
+ {(checkMeta.risk ||
+ checkMeta.description ||
+ showOverviewFindingContent) && (
+
+ {checkMeta.risk && (
+
+
+ Risk:
+
+ {checkMeta.risk}
+
+ )}
+ {checkMeta.description && (
+
+
+ Description:
+
- {checkMeta.remediation.recommendation.text}
+ {checkMeta.description}
- {checkMeta.remediation.recommendation.url && (
-
- View in Prowler Hub
-
- )}
+ )}
+ {showOverviewFindingContent && f?.statusExtended && (
+
+
+ Status Extended:
+
+
+ {f.statusExtended}
+
+
+ )}
+
+ )}
+
+ {/* Card 2: Remediation + Commands */}
+ {(checkMeta.remediation.recommendation.text ||
+ checkMeta.remediation.code.cli ||
+ checkMeta.remediation.code.terraform ||
+ checkMeta.remediation.code.nativeiac) && (
+
+ {checkMeta.remediation.recommendation.text && (
+
+
+ Remediation:
+
+
+
+
+ {checkMeta.remediation.recommendation.text}
+
+
+ {checkMeta.remediation.recommendation.url && (
+
+ View in Prowler Hub
+
+ )}
+
+
+ )}
+
+ {checkMeta.remediation.code.cli && (
+
+ {renderRemediationCodeBlock({
+ label: "CLI Command",
+ language: QUERY_EDITOR_LANGUAGE.SHELL,
+ value: `$ ${stripCodeFences(checkMeta.remediation.code.cli)}`,
+ copyValue: stripCodeFences(
+ checkMeta.remediation.code.cli,
+ ),
+ })}
+
+ )}
+
+ {checkMeta.remediation.code.terraform && (
+
+ {renderRemediationCodeBlock({
+ label: "Terraform",
+ language: QUERY_EDITOR_LANGUAGE.HCL,
+ value: stripCodeFences(
+ checkMeta.remediation.code.terraform,
+ ),
+ })}
+
+ )}
+
+ {checkMeta.remediation.code.nativeiac && providerType && (
+
+ {renderRemediationCodeBlock({
+ label: nativeIacConfig.label,
+ language: nativeIacConfig.language,
+ value: stripCodeFences(
+ checkMeta.remediation.code.nativeiac,
+ ),
+ })}
+
+ )}
+
+ {checkMeta.remediation.code.other && (
+
+
+ Remediation Steps:
+
+
+ {checkMeta.remediation.code.other}
+
+
+ )}
+
+ )}
+
+ {checkMeta.additionalUrls.length > 0 && (
+
+
+
+ References:
+
+
+ {checkMeta.additionalUrls.map((link, idx) => (
+
+
+ {link}
+
+
+ ))}
+
-
+
)}
- {checkMeta.remediation.code.cli && (
-
- {renderRemediationCodeBlock({
- label: "CLI Command",
- value: `$ ${stripCodeFences(checkMeta.remediation.code.cli)}`,
- copyValue: stripCodeFences(
- checkMeta.remediation.code.cli,
- ),
- })}
-
+ {checkMeta.categories.length > 0 && (
+
+
+
+ Categories:
+
+
+ {checkMeta.categories.map((category) => (
+
+ {category}
+
+ ))}
+
+
+
)}
-
- {checkMeta.remediation.code.terraform && (
-
- {renderRemediationCodeBlock({
- label: "Terraform",
- value: stripCodeFences(
- checkMeta.remediation.code.terraform,
- ),
- })}
-
- )}
-
- {checkMeta.remediation.code.nativeiac && (
-
- {renderRemediationCodeBlock({
- label: "CloudFormation",
- value: stripCodeFences(
- checkMeta.remediation.code.nativeiac,
- ),
- })}
-
- )}
-
- {checkMeta.remediation.code.other && (
-
-
- Remediation Steps:
-
-
- {checkMeta.remediation.code.other}
-
-
- )}
-
- )}
-
- {checkMeta.additionalUrls.length > 0 && (
-
-
-
- References:
-
-
- {checkMeta.additionalUrls.map((link, idx) => (
-
-
- {link}
-
-
- ))}
-
-
-
- )}
-
- {checkMeta.categories.length > 0 && (
-
-
-
- Categories:
-
-
- {checkMeta.categories.map((category) => (
-
- {category}
-
- ))}
-
-
-
+ >
+ ) : (
+
)}
@@ -834,16 +950,21 @@ export function ResourceDetailDrawerContent({
value="other-findings"
className="minimal-scrollbar flex flex-col gap-2 overflow-y-auto"
>
- {!f || isNavigating ? (
-
-
-
+ {!f && !isNavigating ? (
+
) : (
<>
-
- {otherFindings.length} Total Entries
-
+ {isNavigating ? (
+
+ ) : (
+
+ {otherFindings.length} Total Entries
+
+ )}
@@ -874,7 +995,9 @@ export function ResourceDetailDrawerContent({
- {otherFindings.length > 0 ? (
+ {isNavigating ? (
+
+ ) : otherFindings.length > 0 ? (
otherFindings.map((finding) => (
- No other findings for this resource.
+ {showSyntheticResourceHint
+ ? "No other findings are available for this IaC resource."
+ : "No other findings for this resource."}
@@ -906,7 +1031,13 @@ export function ResourceDetailDrawerContent({
{/* Scans Tab */}
- {f?.scan ? (
+ {!f && !isNavigating ? (
+
+ Scan information is not available.
+
+ ) : isNavigating ? (
+
+ ) : (
<>
@@ -914,7 +1045,7 @@ export function ResourceDetailDrawerContent({
@@ -925,49 +1056,53 @@ export function ResourceDetailDrawerContent({
- {f.scan.name || "N/A"}
+ {f?.scan?.name || "N/A"}
- {f.scan.uniqueResourceCount}
+ {f?.scan?.uniqueResourceCount}
- {f.scan.progress}%
+ {f?.scan?.progress}%
- {f.scan.trigger}
+ {f?.scan?.trigger}
- {f.scan.state}
+ {f?.scan?.state}
- {formatDuration(f.scan.duration)}
+ {f?.scan?.duration !== undefined
+ ? formatDuration(f.scan.duration)
+ : "-"}
-
+
-
+
-
+
- {f.scan.scheduledAt && (
+ {f?.scan?.scheduledAt && (
)}
>
- ) : (
-
- Scan information is not available.
-
)}
@@ -976,25 +1111,165 @@ export function ResourceDetailDrawerContent({
value="events"
className="flex min-h-0 flex-1 flex-col gap-4"
>
-
+ {isNavigating ? (
+
+ ) : (
+ <>
+
+ >
+ )}
{/* Lighthouse AI button */}
-
-
- Analyze This Finding With Lighthouse AI
-
+ {!isNavigating && (
+
+
+ Analyze This Finding With Lighthouse AI
+
+ )}
+
+ );
+}
+
+function OverviewNavigationSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function OverviewCardSkeleton({ lineWidths }: { lineWidths: string[] }) {
+ return (
+
+ {lineWidths.map((lineWidth, index) => (
+
+ ))}
+
+ );
+}
+
+function OtherFindingsNavigationSkeletonRows() {
+ return (
+ <>
+ {Array.from({ length: 3 }).map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+ >
+ );
+}
+
+function ScansNavigationSkeleton() {
+ return (
+
+
+
+ Showing the latest scan that evaluated this finding
+
+
+
+
+
+
+
+
+ );
+}
+
+function ScansInfoGridSkeleton({ labels }: { labels: string[] }) {
+ const columnCount = labels.length;
+
+ return (
+
+ {labels.map((label, index) => (
+
+ {label}
+
+
+ ))}
+
+ );
+}
+
+function EventsNavigationSkeleton() {
+ return (
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
+ ))}
);
}
@@ -1038,7 +1313,17 @@ function OtherFindingRow({
onClick={() => window.open(findingUrl, "_blank", "noopener,noreferrer")}
>
-
+
diff --git a/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer.tsx b/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer.tsx
index b8837ea596..65ad4e734c 100644
--- a/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer.tsx
+++ b/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer.tsx
@@ -11,6 +11,7 @@ import {
DrawerHeader,
DrawerTitle,
} from "@/components/shadcn";
+import type { FindingResourceRow } from "@/types";
import { ResourceDetailDrawerContent } from "./resource-detail-drawer-content";
import type { CheckMeta } from "./use-resource-detail-drawer";
@@ -23,8 +24,10 @@ interface ResourceDetailDrawerProps {
checkMeta: CheckMeta | null;
currentIndex: number;
totalResources: number;
+ currentResource: FindingResourceRow | null;
currentFinding: ResourceDrawerFinding | null;
otherFindings: ResourceDrawerFinding[];
+ showSyntheticResourceHint?: boolean;
onNavigatePrev: () => void;
onNavigateNext: () => void;
onMuteComplete: () => void;
@@ -38,8 +41,10 @@ export function ResourceDetailDrawer({
checkMeta,
currentIndex,
totalResources,
+ currentResource,
currentFinding,
otherFindings,
+ showSyntheticResourceHint = false,
onNavigatePrev,
onNavigateNext,
onMuteComplete,
@@ -64,8 +69,10 @@ export function ResourceDetailDrawer({
checkMeta={checkMeta}
currentIndex={currentIndex}
totalResources={totalResources}
+ currentResource={currentResource}
currentFinding={currentFinding}
otherFindings={otherFindings}
+ showSyntheticResourceHint={showSyntheticResourceHint}
onNavigatePrev={onNavigatePrev}
onNavigateNext={onNavigateNext}
onMuteComplete={onMuteComplete}
diff --git a/ui/components/findings/table/resource-detail-drawer/use-resource-detail-drawer.test.ts b/ui/components/findings/table/resource-detail-drawer/use-resource-detail-drawer.test.ts
index 3e4d1ef540..9c5a731a51 100644
--- a/ui/components/findings/table/resource-detail-drawer/use-resource-detail-drawer.test.ts
+++ b/ui/components/findings/table/resource-detail-drawer/use-resource-detail-drawer.test.ts
@@ -6,14 +6,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
// ---------------------------------------------------------------------------
const {
+ getFindingByIdMock,
getLatestFindingsByResourceUidMock,
adaptFindingsByResourceResponseMock,
} = vi.hoisted(() => ({
+ getFindingByIdMock: vi.fn(),
getLatestFindingsByResourceUidMock: vi.fn(),
adaptFindingsByResourceResponseMock: vi.fn(),
}));
vi.mock("@/actions/findings", () => ({
+ getFindingById: getFindingByIdMock,
getLatestFindingsByResourceUid: getLatestFindingsByResourceUidMock,
adaptFindingsByResourceResponse: adaptFindingsByResourceResponseMock,
}));
@@ -109,6 +112,7 @@ describe("useResourceDetailDrawer — unmount cleanup", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
+ getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
});
it("should abort the in-flight fetch controller when the hook unmounts", async () => {
@@ -116,9 +120,7 @@ describe("useResourceDetailDrawer — unmount cleanup", () => {
const abortSpy = vi.spyOn(AbortController.prototype, "abort");
// never-resolving fetch to simulate in-flight request
- getLatestFindingsByResourceUidMock.mockImplementation(
- () => new Promise(() => {}),
- );
+ getFindingByIdMock.mockImplementation(() => new Promise(() => {}));
adaptFindingsByResourceResponseMock.mockReturnValue([]);
const resources = [makeResource()];
@@ -126,7 +128,6 @@ describe("useResourceDetailDrawer — unmount cleanup", () => {
const { result, unmount } = renderHook(() =>
useResourceDetailDrawer({
resources,
- checkId: "s3_check",
}),
);
@@ -136,7 +137,7 @@ describe("useResourceDetailDrawer — unmount cleanup", () => {
});
// Verify a fetch was started
- expect(getLatestFindingsByResourceUidMock).toHaveBeenCalledTimes(1);
+ expect(getFindingByIdMock).toHaveBeenCalledTimes(1);
// Reset spy count to detect only the unmount abort
abortSpy.mockClear();
@@ -158,7 +159,6 @@ describe("useResourceDetailDrawer — unmount cleanup", () => {
const { unmount } = renderHook(() =>
useResourceDetailDrawer({
resources,
- checkId: "s3_check",
}),
);
@@ -173,40 +173,59 @@ describe("useResourceDetailDrawer — unmount cleanup", () => {
describe("useResourceDetailDrawer — other findings filtering", () => {
beforeEach(() => {
vi.clearAllMocks();
+ getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
});
- it("should exclude the current finding from otherFindings and preserve API order", async () => {
+ it("should load other findings from the current resource uid and exclude only the current finding (status is filtered server-side)", async () => {
const resources = [makeResource()];
- getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
- adaptFindingsByResourceResponseMock.mockReturnValue([
- makeDrawerFinding({
- id: "current",
- checkId: "s3_check",
- checkTitle: "Current",
- status: "FAIL",
- severity: "critical",
- }),
- makeDrawerFinding({
- id: "other-1",
- checkId: "check-other-1",
- checkTitle: "Other 1",
- status: "PASS",
- severity: "critical",
- }),
- makeDrawerFinding({
- id: "other-2",
- checkId: "check-other-2",
- checkTitle: "Other 2",
- status: "FAIL",
- severity: "medium",
- }),
- ]);
+ // Given — the API call applies filter[status]=FAIL server-side, so the
+ // mock returns only FAIL rows. The hook's only client-side job is to
+ // drop the row already shown above the table.
+ getFindingByIdMock.mockResolvedValue({ data: ["detail"] });
+ getLatestFindingsByResourceUidMock.mockResolvedValue({
+ data: ["resource"],
+ });
+ adaptFindingsByResourceResponseMock.mockImplementation(
+ (response: { data: string[] }) => {
+ if (response.data[0] === "detail") {
+ return [
+ makeDrawerFinding({
+ id: "finding-1",
+ checkId: "s3_check",
+ checkTitle: "Current",
+ status: "FAIL",
+ severity: "informational",
+ }),
+ ];
+ }
+
+ return [
+ makeDrawerFinding({
+ id: "finding-3",
+ checkTitle: "First other finding",
+ status: "FAIL",
+ severity: "high",
+ }),
+ makeDrawerFinding({
+ id: "finding-1",
+ checkTitle: "Current finding duplicate from resource fetch",
+ status: "FAIL",
+ severity: "critical",
+ }),
+ makeDrawerFinding({
+ id: "finding-5",
+ checkTitle: "Second other finding",
+ status: "FAIL",
+ severity: "medium",
+ }),
+ ];
+ },
+ );
const { result } = renderHook(() =>
useResourceDetailDrawer({
resources,
- checkId: "s3_check",
}),
);
@@ -215,12 +234,92 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
await Promise.resolve();
});
+ // Then
+ expect(getFindingByIdMock).toHaveBeenCalledWith(
+ "finding-1",
+ "resources,scan.provider",
+ { source: "resource-detail-drawer" },
+ );
+ expect(getLatestFindingsByResourceUidMock).toHaveBeenCalledWith({
+ resourceUid: "arn:aws:s3:::my-bucket",
+ pageSize: 50,
+ includeMuted: false,
+ });
+ expect(result.current.currentFinding?.id).toBe("finding-1");
expect(result.current.otherFindings.map((finding) => finding.id)).toEqual([
- "other-1",
- "other-2",
+ "finding-3",
+ "finding-5",
]);
});
+ it("should skip loading other findings for synthetic IaC resources and keep the current detail on findingId", async () => {
+ const resources = [
+ makeResource({
+ findingId: "synthetic-finding",
+ resourceUid: "synthetic://iac-resource",
+ }),
+ ];
+
+ // Given
+ getFindingByIdMock.mockResolvedValue({ data: ["detail"] });
+ adaptFindingsByResourceResponseMock.mockReturnValue([
+ makeDrawerFinding({
+ id: "synthetic-finding",
+ checkId: "s3_check",
+ status: "MANUAL",
+ severity: "informational",
+ }),
+ ]);
+
+ const { result } = renderHook(() =>
+ useResourceDetailDrawer({
+ resources,
+ canLoadOtherFindings: false,
+ }),
+ );
+
+ await act(async () => {
+ // When
+ result.current.openDrawer(0);
+ await Promise.resolve();
+ });
+
+ // Then
+ expect(getFindingByIdMock).toHaveBeenCalledWith(
+ "synthetic-finding",
+ "resources,scan.provider",
+ { source: "resource-detail-drawer" },
+ );
+ expect(getLatestFindingsByResourceUidMock).not.toHaveBeenCalled();
+ expect(result.current.currentFinding?.id).toBe("synthetic-finding");
+ expect(result.current.otherFindings).toEqual([]);
+ });
+
+ it("should request muted findings only when explicitly enabled", async () => {
+ const resources = [makeResource()];
+
+ getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
+ adaptFindingsByResourceResponseMock.mockReturnValue([makeDrawerFinding()]);
+
+ const { result } = renderHook(() =>
+ useResourceDetailDrawer({
+ resources,
+ includeMutedInOtherFindings: true,
+ }),
+ );
+
+ await act(async () => {
+ result.current.openDrawer(0);
+ await Promise.resolve();
+ });
+
+ expect(getLatestFindingsByResourceUidMock).toHaveBeenCalledWith({
+ resourceUid: "arn:aws:s3:::my-bucket",
+ pageSize: 50,
+ includeMuted: true,
+ });
+ });
+
it("should keep isNavigating true for a cached resource long enough to render skeletons", async () => {
vi.useFakeTimers();
@@ -239,19 +338,19 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
}),
];
- getLatestFindingsByResourceUidMock.mockImplementation(
- async ({ resourceUid }: { resourceUid: string }) => ({
- data: [resourceUid],
- }),
- );
+ getFindingByIdMock.mockImplementation(async (findingId: string) => ({
+ data: [findingId],
+ }));
adaptFindingsByResourceResponseMock.mockImplementation(
(response: { data: string[] }) => [
makeDrawerFinding({
- id: response.data[0].includes("first") ? "finding-1" : "finding-2",
- resourceUid: response.data[0],
- resourceName: response.data[0].includes("first")
- ? "first-bucket"
- : "second-bucket",
+ id: response.data[0],
+ resourceUid:
+ response.data[0] === "finding-1"
+ ? "arn:aws:s3:::first-bucket"
+ : "arn:aws:s3:::second-bucket",
+ resourceName:
+ response.data[0] === "finding-1" ? "first-bucket" : "second-bucket",
}),
],
);
@@ -259,7 +358,6 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
const { result } = renderHook(() =>
useResourceDetailDrawer({
resources,
- checkId: "s3_check",
}),
);
@@ -284,6 +382,8 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
expect(result.current.isNavigating).toBe(true);
await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
vi.runAllTimers();
await Promise.resolve();
});
@@ -313,19 +413,19 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
}),
];
- getLatestFindingsByResourceUidMock.mockImplementation(
- async ({ resourceUid }: { resourceUid: string }) => ({
- data: [resourceUid],
- }),
- );
+ getFindingByIdMock.mockImplementation(async (findingId: string) => ({
+ data: [findingId],
+ }));
adaptFindingsByResourceResponseMock.mockImplementation(
(response: { data: string[] }) => [
makeDrawerFinding({
- id: response.data[0].includes("first") ? "finding-1" : "finding-2",
- resourceUid: response.data[0],
- resourceName: response.data[0].includes("first")
- ? "first-bucket"
- : "second-bucket",
+ id: response.data[0],
+ resourceUid:
+ response.data[0] === "finding-1"
+ ? "arn:aws:s3:::first-bucket"
+ : "arn:aws:s3:::second-bucket",
+ resourceName:
+ response.data[0] === "finding-1" ? "first-bucket" : "second-bucket",
}),
],
);
@@ -333,7 +433,6 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
const { result } = renderHook(() =>
useResourceDetailDrawer({
resources,
- checkId: "s3_check",
}),
);
@@ -378,6 +477,154 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
vi.useRealTimers();
});
+ it("should update checkMeta when navigating to a resource with a different check", async () => {
+ // Given
+ const resources = [
+ makeResource({
+ id: "row-1",
+ findingId: "finding-1",
+ checkId: "s3_check",
+ }),
+ makeResource({
+ id: "row-2",
+ findingId: "finding-2",
+ checkId: "ec2_check",
+ resourceUid: "arn:aws:ec2:::instance/i-123",
+ resourceName: "instance-1",
+ service: "ec2",
+ }),
+ ];
+
+ getFindingByIdMock.mockImplementation(async (findingId: string) => ({
+ data: [findingId],
+ }));
+ getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
+ adaptFindingsByResourceResponseMock.mockImplementation(
+ (response: { data: string[] }) => [
+ response.data[0] === "finding-1"
+ ? makeDrawerFinding({
+ id: "finding-1",
+ checkId: "s3_check",
+ checkTitle: "S3 Check",
+ description: "s3 description",
+ })
+ : makeDrawerFinding({
+ id: "finding-2",
+ checkId: "ec2_check",
+ checkTitle: "EC2 Check",
+ description: "ec2 description",
+ }),
+ ],
+ );
+
+ const { result } = renderHook(() =>
+ useResourceDetailDrawer({
+ resources,
+ }),
+ );
+
+ // When
+ await act(async () => {
+ result.current.openDrawer(0);
+ await Promise.resolve();
+ });
+
+ expect(result.current.checkMeta?.checkTitle).toBe("S3 Check");
+
+ await act(async () => {
+ result.current.navigateNext();
+ await Promise.resolve();
+ });
+
+ // Then
+ expect(result.current.checkMeta?.checkTitle).toBe("EC2 Check");
+ expect(result.current.checkMeta?.description).toBe("ec2 description");
+ });
+
+ it("should keep the previous check metadata cached while reopening until the new finding arrives", async () => {
+ // Given
+ const resources = [
+ makeResource({
+ id: "row-1",
+ findingId: "finding-1",
+ checkId: "s3_check",
+ }),
+ makeResource({
+ id: "row-2",
+ findingId: "finding-2",
+ checkId: "ec2_check",
+ resourceUid: "arn:aws:ec2:::instance/i-123",
+ resourceName: "instance-1",
+ service: "ec2",
+ }),
+ ];
+
+ let resolveSecondFinding: ((value: { data: string[] }) => void) | null =
+ null;
+
+ getFindingByIdMock.mockImplementation((findingId: string) => {
+ if (findingId === "finding-2") {
+ return new Promise((resolve) => {
+ resolveSecondFinding = resolve;
+ });
+ }
+
+ return Promise.resolve({ data: [findingId] });
+ });
+ getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
+ adaptFindingsByResourceResponseMock.mockImplementation(
+ (response: { data: string[] }) => [
+ response.data[0] === "finding-1"
+ ? makeDrawerFinding({
+ id: "finding-1",
+ checkId: "s3_check",
+ checkTitle: "S3 Check",
+ description: "s3 description",
+ })
+ : makeDrawerFinding({
+ id: "finding-2",
+ checkId: "ec2_check",
+ checkTitle: "EC2 Check",
+ description: "ec2 description",
+ }),
+ ],
+ );
+
+ const { result } = renderHook(() =>
+ useResourceDetailDrawer({
+ resources,
+ }),
+ );
+
+ await act(async () => {
+ result.current.openDrawer(0);
+ await Promise.resolve();
+ });
+
+ expect(result.current.checkMeta?.checkTitle).toBe("S3 Check");
+
+ // When
+ act(() => {
+ result.current.closeDrawer();
+ result.current.openDrawer(1);
+ });
+
+ // Then
+ expect(result.current.isOpen).toBe(true);
+ expect(result.current.currentIndex).toBe(1);
+ expect(result.current.currentFinding).toBeNull();
+ expect(result.current.checkMeta?.checkTitle).toBe("S3 Check");
+
+ await act(async () => {
+ resolveSecondFinding?.({ data: ["finding-2"] });
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(result.current.checkMeta?.checkTitle).toBe("EC2 Check");
+ expect(result.current.checkMeta?.description).toBe("ec2 description");
+ });
+
it("should clear the previous resource findings when navigation to the next resource fails", async () => {
// Given
const resources = [
@@ -395,24 +642,24 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
}),
];
- getLatestFindingsByResourceUidMock.mockImplementation(
- async ({ resourceUid }: { resourceUid: string }) => {
- if (resourceUid.includes("second")) {
- throw new Error("Fetch failed");
- }
+ getFindingByIdMock.mockImplementation(async (findingId: string) => {
+ if (findingId === "finding-2") {
+ throw new Error("Fetch failed");
+ }
- return { data: [resourceUid] };
- },
- );
+ return { data: [findingId] };
+ });
adaptFindingsByResourceResponseMock.mockImplementation(
(response: { data: string[] }) => [
makeDrawerFinding({
- id: response.data[0].includes("first") ? "finding-1" : "finding-2",
- resourceUid: response.data[0],
- resourceName: response.data[0].includes("first")
- ? "first-bucket"
- : "second-bucket",
+ id: response.data[0],
+ resourceUid:
+ response.data[0] === "finding-1"
+ ? "arn:aws:s3:::first-bucket"
+ : "arn:aws:s3:::second-bucket",
+ resourceName:
+ response.data[0] === "finding-1" ? "first-bucket" : "second-bucket",
}),
],
);
@@ -420,7 +667,6 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
const { result } = renderHook(() =>
useResourceDetailDrawer({
resources,
- checkId: "s3_check",
}),
);
@@ -432,6 +678,7 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
expect(result.current.currentFinding?.resourceUid).toBe(
"arn:aws:s3:::first-bucket",
);
+ expect(result.current.checkMeta?.checkTitle).toBe("S3 Check");
// When
await act(async () => {
@@ -443,5 +690,123 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
expect(result.current.currentIndex).toBe(1);
expect(result.current.currentFinding).toBeNull();
expect(result.current.otherFindings).toEqual([]);
+ expect(result.current.checkMeta).toBeNull();
+ });
+
+ it("should clear other findings immediately while the next resource is loading", async () => {
+ // Given
+ const resources = [
+ makeResource({
+ id: "row-1",
+ findingId: "finding-1",
+ resourceUid: "arn:aws:s3:::first-bucket",
+ resourceName: "first-bucket",
+ }),
+ makeResource({
+ id: "row-2",
+ findingId: "finding-2",
+ resourceUid: "arn:aws:s3:::second-bucket",
+ resourceName: "second-bucket",
+ }),
+ ];
+
+ let resolveSecondFinding: ((value: { data: string[] }) => void) | null =
+ null;
+ let resolveSecondResource: ((value: { data: string[] }) => void) | null =
+ null;
+
+ getFindingByIdMock.mockImplementation((findingId: string) => {
+ if (findingId === "finding-2") {
+ return new Promise((resolve) => {
+ resolveSecondFinding = resolve;
+ });
+ }
+
+ return Promise.resolve({ data: [findingId] });
+ });
+
+ getLatestFindingsByResourceUidMock.mockImplementation(
+ ({ resourceUid }: { resourceUid: string }) => {
+ if (resourceUid === "arn:aws:s3:::second-bucket") {
+ return new Promise((resolve) => {
+ resolveSecondResource = resolve;
+ });
+ }
+
+ return Promise.resolve({ data: ["resource-1"] });
+ },
+ );
+
+ adaptFindingsByResourceResponseMock.mockImplementation(
+ (response: { data: string[] }) => {
+ if (response.data[0] === "finding-1") {
+ return [makeDrawerFinding({ id: "finding-1" })];
+ }
+
+ if (response.data[0] === "finding-2") {
+ return [
+ makeDrawerFinding({
+ id: "finding-2",
+ resourceUid: "arn:aws:s3:::second-bucket",
+ resourceName: "second-bucket",
+ }),
+ ];
+ }
+
+ if (response.data[0] === "resource-1") {
+ return [
+ makeDrawerFinding({
+ id: "finding-3",
+ checkTitle: "First bucket other finding",
+ resourceUid: "arn:aws:s3:::first-bucket",
+ }),
+ ];
+ }
+
+ return [
+ makeDrawerFinding({
+ id: "finding-4",
+ checkTitle: "Second bucket other finding",
+ resourceUid: "arn:aws:s3:::second-bucket",
+ }),
+ ];
+ },
+ );
+
+ const { result } = renderHook(() =>
+ useResourceDetailDrawer({
+ resources,
+ }),
+ );
+
+ await act(async () => {
+ result.current.openDrawer(0);
+ await Promise.resolve();
+ });
+
+ expect(result.current.otherFindings.map((finding) => finding.id)).toEqual([
+ "finding-3",
+ ]);
+
+ // When
+ act(() => {
+ result.current.navigateNext();
+ });
+
+ // Then
+ expect(result.current.currentIndex).toBe(1);
+ expect(result.current.currentFinding).toBeNull();
+ expect(result.current.otherFindings).toEqual([]);
+
+ await act(async () => {
+ resolveSecondFinding?.({ data: ["finding-2"] });
+ resolveSecondResource?.({ data: ["resource-2"] });
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(result.current.otherFindings.map((finding) => finding.id)).toEqual([
+ "finding-4",
+ ]);
});
});
diff --git a/ui/components/findings/table/resource-detail-drawer/use-resource-detail-drawer.ts b/ui/components/findings/table/resource-detail-drawer/use-resource-detail-drawer.ts
index f092c606dc..1a4a9c37f9 100644
--- a/ui/components/findings/table/resource-detail-drawer/use-resource-detail-drawer.ts
+++ b/ui/components/findings/table/resource-detail-drawer/use-resource-detail-drawer.ts
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
import {
adaptFindingsByResourceResponse,
+ getFindingById,
getLatestFindingsByResourceUid,
type ResourceDrawerFinding,
} from "@/actions/findings";
@@ -43,9 +44,11 @@ function extractCheckMeta(finding: ResourceDrawerFinding): CheckMeta {
interface UseResourceDetailDrawerOptions {
resources: FindingResourceRow[];
- checkId: string;
totalResourceCount?: number;
onRequestMoreResources?: () => void;
+ initialIndex?: number | null;
+ canLoadOtherFindings?: boolean;
+ includeMutedInOtherFindings?: boolean;
}
interface UseResourceDetailDrawerReturn {
@@ -55,9 +58,9 @@ interface UseResourceDetailDrawerReturn {
checkMeta: CheckMeta | null;
currentIndex: number;
totalResources: number;
+ currentResource: FindingResourceRow | null;
currentFinding: ResourceDrawerFinding | null;
otherFindings: ResourceDrawerFinding[];
- allFindings: ResourceDrawerFinding[];
openDrawer: (index: number) => void;
closeDrawer: () => void;
navigatePrev: () => void;
@@ -69,22 +72,33 @@ interface UseResourceDetailDrawerReturn {
/**
* Manages the resource detail drawer state, fetching, and navigation.
*
- * Caches findings per resourceUid in a Map ref so navigating prev/next
+ * Caches findings per findingId in a Map ref so navigating prev/next
* doesn't re-fetch already-visited resources.
*/
export function useResourceDetailDrawer({
resources,
- checkId,
totalResourceCount,
onRequestMoreResources,
+ initialIndex = null,
+ canLoadOtherFindings = true,
+ includeMutedInOtherFindings = false,
}: UseResourceDetailDrawerOptions): UseResourceDetailDrawerReturn {
- const [isOpen, setIsOpen] = useState(false);
+ const [isOpen, setIsOpen] = useState(initialIndex !== null);
const [isLoading, setIsLoading] = useState(false);
- const [currentIndex, setCurrentIndex] = useState(0);
- const [findings, setFindings] = useState([]);
+ const [currentIndex, setCurrentIndex] = useState(initialIndex ?? 0);
+ const [currentFinding, setCurrentFinding] =
+ useState(null);
+ const [otherFindings, setOtherFindings] = useState(
+ [],
+ );
const [isNavigating, setIsNavigating] = useState(false);
- const cacheRef = useRef>(new Map());
+ const currentFindingCacheRef = useRef<
+ Map
+ >(new Map());
+ const otherFindingsCacheRef = useRef>(
+ new Map(),
+ );
const checkMetaRef = useRef(null);
const fetchControllerRef = useRef(null);
const navigationTimeoutRef = useRef | null>(
@@ -132,6 +146,11 @@ export function useResourceDetailDrawer({
setIsNavigating(true);
};
+ const resetCurrentResourceState = () => {
+ setCurrentFinding(null);
+ setOtherFindings([]);
+ };
+
// Abort any in-flight request on unmount to prevent state updates
// on an already-unmounted component.
useEffect(() => {
@@ -142,46 +161,83 @@ export function useResourceDetailDrawer({
};
}, []);
- const fetchFindings = async (resourceUid: string) => {
+ const fetchFindings = async (resource: FindingResourceRow) => {
// Abort any in-flight request to prevent stale data from out-of-order responses
fetchControllerRef.current?.abort();
clearNavigationTimeout();
const controller = new AbortController();
fetchControllerRef.current = controller;
- // Check cache first
- const cached = cacheRef.current.get(resourceUid);
- if (cached) {
- if (!checkMetaRef.current) {
- const main = cached.find((f) => f.checkId === checkId) ?? cached[0];
- if (main) checkMetaRef.current = extractCheckMeta(main);
+ const { findingId, resourceUid } = resource;
+
+ const fetchCurrentFinding = async () => {
+ const cached = currentFindingCacheRef.current.get(findingId);
+ if (cached !== undefined) {
+ return cached;
}
- setFindings(cached);
- finishNavigation();
- return;
- }
+
+ const response = await getFindingById(
+ findingId,
+ "resources,scan.provider",
+ { source: "resource-detail-drawer" },
+ );
+
+ const adapted = adaptFindingsByResourceResponse(response);
+ const finding =
+ adapted.find((item) => item.id === findingId) ?? adapted[0] ?? null;
+
+ currentFindingCacheRef.current.set(findingId, finding);
+
+ return finding;
+ };
+
+ const fetchOtherFindings = async () => {
+ if (!canLoadOtherFindings || !resourceUid) {
+ return [];
+ }
+
+ const cached = otherFindingsCacheRef.current.get(resourceUid);
+ if (cached) {
+ return cached;
+ }
+
+ const response = await getLatestFindingsByResourceUid({
+ resourceUid,
+ pageSize: 50,
+ includeMuted: includeMutedInOtherFindings,
+ });
+ const adapted = adaptFindingsByResourceResponse(response);
+
+ otherFindingsCacheRef.current.set(resourceUid, adapted);
+
+ return adapted;
+ };
setIsLoading(true);
try {
- const response = await getLatestFindingsByResourceUid({ resourceUid });
+ const [nextCurrentFinding, nextOtherFindings] = await Promise.all([
+ fetchCurrentFinding(),
+ fetchOtherFindings(),
+ ]);
// Discard stale response if a newer request was started
if (controller.signal.aborted) return;
- const adapted = adaptFindingsByResourceResponse(response);
- cacheRef.current.set(resourceUid, adapted);
+ checkMetaRef.current = nextCurrentFinding
+ ? extractCheckMeta(nextCurrentFinding)
+ : null;
- // Extract check-level metadata once (stable across all resources)
- if (!checkMetaRef.current) {
- const main = adapted.find((f) => f.checkId === checkId) ?? adapted[0];
- if (main) checkMetaRef.current = extractCheckMeta(main);
- }
-
- setFindings(adapted);
- } catch (error) {
+ setCurrentFinding(nextCurrentFinding);
+ // The API already filters to status=FAIL (see getLatestFindingsByResourceUid).
+ // Only need to drop the current finding from the list.
+ setOtherFindings(
+ nextOtherFindings.filter((finding) => finding.id !== findingId),
+ );
+ } catch (_error) {
if (!controller.signal.aborted) {
- console.error("Error fetching findings for resource:", error);
- setFindings([]);
+ checkMetaRef.current = null;
+ setCurrentFinding(null);
+ setOtherFindings([]);
}
} finally {
if (!controller.signal.aborted) {
@@ -190,17 +246,31 @@ export function useResourceDetailDrawer({
}
};
+ useEffect(() => {
+ if (initialIndex === null) {
+ return;
+ }
+
+ const resource = resources[initialIndex];
+ if (!resource) {
+ return;
+ }
+
+ fetchFindings(resource);
+ // Only initialize once on mount for deep-link/inline entry points.
+ // User-driven navigations use openDrawer/navigateTo afterwards.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const openDrawer = (index: number) => {
const resource = resources[index];
if (!resource) return;
- clearNavigationTimeout();
- navigationStartedAtRef.current = null;
setCurrentIndex(index);
setIsOpen(true);
- setIsNavigating(false);
- setFindings([]);
- fetchFindings(resource.resourceUid);
+ startNavigation();
+ resetCurrentResourceState();
+ fetchFindings(resource);
};
const closeDrawer = () => {
@@ -210,10 +280,11 @@ export function useResourceDetailDrawer({
const refetchCurrent = () => {
const resource = resources[currentIndex];
if (!resource) return;
- cacheRef.current.delete(resource.resourceUid);
+ currentFindingCacheRef.current.delete(resource.findingId);
+ otherFindingsCacheRef.current.delete(resource.resourceUid);
startNavigation();
- setFindings([]);
- fetchFindings(resource.resourceUid);
+ resetCurrentResourceState();
+ fetchFindings(resource);
};
const navigateTo = (index: number) => {
@@ -222,8 +293,8 @@ export function useResourceDetailDrawer({
setCurrentIndex(index);
startNavigation();
- setFindings([]);
- fetchFindings(resource.resourceUid);
+ resetCurrentResourceState();
+ fetchFindings(resource);
};
const navigatePrev = () => {
@@ -247,14 +318,7 @@ export function useResourceDetailDrawer({
}
};
- // The finding whose checkId matches the drill-down's checkId
- const currentFinding =
- findings.find((f) => f.checkId === checkId) ?? findings[0] ?? null;
-
- // All other findings for this resource
- const otherFindings = currentFinding
- ? findings.filter((f) => f.id !== currentFinding.id)
- : findings;
+ const currentResource = resources[currentIndex];
return {
isOpen,
@@ -263,9 +327,9 @@ export function useResourceDetailDrawer({
checkMeta: checkMetaRef.current,
currentIndex,
totalResources: totalResourceCount ?? resources.length,
+ currentResource: currentResource ?? null,
currentFinding,
otherFindings,
- allFindings: findings,
openDrawer,
closeDrawer,
navigatePrev,
diff --git a/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.test.tsx b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.test.tsx
new file mode 100644
index 0000000000..fff67495d4
--- /dev/null
+++ b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.test.tsx
@@ -0,0 +1,48 @@
+import { render, screen } from "@testing-library/react";
+import type { ReactNode } from "react";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock("next/link", () => ({
+ default: ({
+ children,
+ href,
+ ...rest
+ }: {
+ children: ReactNode;
+ href: string;
+ "aria-label"?: string;
+ className?: string;
+ }) => (
+
+ {children}
+
+ ),
+}));
+
+import { LinkToFindings } from "./link-to-findings";
+
+describe("LinkToFindings", () => {
+ it("should link to findings sorted by severity (desc) then last_seen_at (desc), filtered to FAIL + new delta", () => {
+ render( );
+
+ const link = screen.getByRole("link", { name: "Go to Findings page" });
+ const href = link.getAttribute("href") ?? "";
+ const [, query = ""] = href.split("?");
+ const params = new URLSearchParams(query);
+
+ expect(params.get("sort")).toBe("-severity,-last_seen_at");
+ expect(params.get("filter[status__in]")).toBe("FAIL");
+ // filter[delta] must be singular — the finding-groups filter does not
+ // register `delta__in`, so the plural form is silently dropped by the API.
+ expect(params.get("filter[delta]")).toBe("new");
+ expect(params.has("filter[delta__in]")).toBe(false);
+ });
+
+ it("should render as a tertiary text link (not a solid button) to match the overview Card pattern", () => {
+ render( );
+
+ const link = screen.getByRole("link", { name: "Go to Findings page" });
+ expect(link.className).toContain("text-button-tertiary");
+ expect(link.className).toContain("hover:text-button-tertiary-hover");
+ });
+});
diff --git a/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx
index 74272133ce..41b9f69e75 100644
--- a/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx
+++ b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx
@@ -1,20 +1,13 @@
-"use client";
-
import Link from "next/link";
-import { Button } from "@/components/shadcn/button/button";
-
export const LinkToFindings = () => {
return (
-
-
-
- Check out on Findings
-
-
-
+
+ Check out on Findings
+
);
};
diff --git a/ui/components/overview/new-findings-table/table/column-latest-findings.tsx b/ui/components/overview/new-findings-table/table/column-latest-findings.tsx
new file mode 100644
index 0000000000..6310e2dc40
--- /dev/null
+++ b/ui/components/overview/new-findings-table/table/column-latest-findings.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import { ColumnDef } from "@tanstack/react-table";
+
+import { getStandaloneFindingColumns } from "@/components/findings/table/column-standalone-findings";
+import { FindingProps } from "@/types";
+
+export const ColumnLatestFindings: ColumnDef[] =
+ getStandaloneFindingColumns({
+ includeUpdatedAt: true,
+ });
diff --git a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx
deleted file mode 100644
index 94eeca391d..0000000000
--- a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-"use client";
-
-import { ColumnDef, RowSelectionState } from "@tanstack/react-table";
-
-import { getColumnFindings } from "@/components/findings/table/column-findings";
-import { FindingProps } from "@/types";
-
-const baseColumns: ColumnDef[] = getColumnFindings(
- {} as RowSelectionState,
- 0,
-).filter((column) => column.id !== "select" && column.id !== "actions");
-
-export const ColumnNewFindingsToDate: ColumnDef[] = baseColumns;
diff --git a/ui/components/overview/new-findings-table/table/index.ts b/ui/components/overview/new-findings-table/table/index.ts
index 5f8e56989e..7065a82bf8 100644
--- a/ui/components/overview/new-findings-table/table/index.ts
+++ b/ui/components/overview/new-findings-table/table/index.ts
@@ -1,2 +1,2 @@
-export * from "./column-new-findings-to-date";
+export * from "./column-latest-findings";
export * from "./skeleton-table-new-findings";
diff --git a/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx b/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx
index 7783e7ab2d..df5017712e 100644
--- a/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx
+++ b/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx
@@ -1,39 +1,114 @@
-import React from "react";
-
-import { Card } from "@/components/shadcn/card/card";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
-export const SkeletonTableNewFindings = () => {
- const columns = 7;
- const rows = 3;
-
+const SkeletonTableRow = () => {
return (
-
- {/* Table headers */}
-
- {Array.from({ length: columns }).map((_, index) => (
-
- ))}
-
-
- {/* Table body */}
-
- {Array.from({ length: rows }).map((_, rowIndex) => (
-
- {Array.from({ length: columns }).map((_, colIndex) => (
-
- ))}
-
- ))}
-
-
+
+ {/* Notification dot */}
+
+
+
+ {/* Status badge */}
+
+
+
+ {/* Finding title */}
+
+
+
+ {/* Resource name */}
+
+
+
+
+
+
+ {/* Severity badge */}
+
+
+
+ {/* Provider icon */}
+
+
+
+ {/* Service */}
+
+
+
+ {/* Region — flag + name */}
+
+
+
+
+
+
+ {/* Time */}
+
+
+
+
+ );
+};
+
+export const SkeletonTableNewFindings = () => {
+ const rows = 10;
+
+ return (
+
+ {/* Header: title + description on the left, link on the right */}
+
+
+ {/* Table */}
+
+
+
+ {/* Notification header (no text) */}
+
+ {/* Status */}
+
+
+
+ {/* Finding */}
+
+
+
+ {/* Resource name */}
+
+
+
+ {/* Severity */}
+
+
+
+ {/* Cloud Provider */}
+
+
+
+ {/* Service */}
+
+
+
+ {/* Region */}
+
+
+
+ {/* Time */}
+
+
+
+
+
+
+ {Array.from({ length: rows }).map((_, i) => (
+
+ ))}
+
+
+
);
};
diff --git a/ui/components/providers/add-provider-button.tsx b/ui/components/providers/add-provider-button.tsx
index 30d630c85a..b296533ccc 100644
--- a/ui/components/providers/add-provider-button.tsx
+++ b/ui/components/providers/add-provider-button.tsx
@@ -5,13 +5,28 @@ import { useState } from "react";
import { ProviderWizardModal } from "@/components/providers/wizard";
import { Button } from "@/components/shadcn";
-export const AddProviderButton = () => {
+interface AddProviderButtonProps {
+ onOpenWizard?: () => void;
+}
+
+export const AddProviderButton = ({ onOpenWizard }: AddProviderButtonProps) => {
const [open, setOpen] = useState(false);
+ const handleOpen = () => {
+ if (onOpenWizard) {
+ onOpenWizard();
+ return;
+ }
+
+ setOpen(true);
+ };
+
return (
<>
- setOpen(true)}>Add Provider
-
+ Add Provider
+ {!onOpenWizard && (
+
+ )}
>
);
};
diff --git a/ui/components/providers/index.ts b/ui/components/providers/index.ts
index 71a18944ef..9184ceab2e 100644
--- a/ui/components/providers/index.ts
+++ b/ui/components/providers/index.ts
@@ -5,6 +5,7 @@ export * from "./forms/delete-form";
export * from "./link-to-scans";
export * from "./muted-findings-config-button";
export * from "./providers-accounts-table";
+export * from "./providers-accounts-view";
export * from "./providers-filters";
export * from "./radio-card";
export * from "./radio-group-provider";
diff --git a/ui/components/providers/providers-accounts-table.tsx b/ui/components/providers/providers-accounts-table.tsx
index 212fd475c9..0c25b609d9 100644
--- a/ui/components/providers/providers-accounts-table.tsx
+++ b/ui/components/providers/providers-accounts-table.tsx
@@ -3,6 +3,10 @@
import { RowSelectionState } from "@tanstack/react-table";
import { useEffect, useState } from "react";
+import type {
+ OrgWizardInitialData,
+ ProviderWizardInitialData,
+} from "@/components/providers/wizard/types";
import { DataTable } from "@/components/ui/table";
import { MetaDataProps } from "@/types";
import {
@@ -16,6 +20,8 @@ interface ProvidersAccountsTableProps {
isCloud: boolean;
metadata?: MetaDataProps;
rows: ProvidersTableRow[];
+ onOpenProviderWizard: (initialData?: ProviderWizardInitialData) => void;
+ onOpenOrganizationWizard: (initialData: OrgWizardInitialData) => void;
}
function computeTestableProviderIds(
@@ -48,6 +54,8 @@ export function ProvidersAccountsTable({
isCloud,
metadata,
rows,
+ onOpenProviderWizard,
+ onOpenOrganizationWizard,
}: ProvidersAccountsTableProps) {
const [rowSelection, setRowSelection] = useState({});
@@ -65,6 +73,8 @@ export function ProvidersAccountsTable({
rowSelection,
testableProviderIds,
clearSelection,
+ onOpenProviderWizard,
+ onOpenOrganizationWizard,
);
return (
diff --git a/ui/components/providers/providers-accounts-view.tsx b/ui/components/providers/providers-accounts-view.tsx
new file mode 100644
index 0000000000..51329b5515
--- /dev/null
+++ b/ui/components/providers/providers-accounts-view.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { useState } from "react";
+
+import { AddProviderButton } from "@/components/providers/add-provider-button";
+import { MutedFindingsConfigButton } from "@/components/providers/muted-findings-config-button";
+import { ProvidersAccountsTable } from "@/components/providers/providers-accounts-table";
+import { ProvidersFilters } from "@/components/providers/providers-filters";
+import { ProviderWizardModal } from "@/components/providers/wizard";
+import type {
+ OrgWizardInitialData,
+ ProviderWizardInitialData,
+} from "@/components/providers/wizard/types";
+import type { FilterOption, MetaDataProps, ProviderProps } from "@/types";
+import type { ProvidersTableRow } from "@/types/providers-table";
+
+interface ProvidersAccountsViewProps {
+ isCloud: boolean;
+ filters: FilterOption[];
+ metadata?: MetaDataProps;
+ providers: ProviderProps[];
+ rows: ProvidersTableRow[];
+}
+
+export function ProvidersAccountsView({
+ isCloud,
+ filters,
+ metadata,
+ providers,
+ rows,
+}: ProvidersAccountsViewProps) {
+ const [isProviderWizardOpen, setIsProviderWizardOpen] = useState(false);
+ const [providerWizardInitialData, setProviderWizardInitialData] = useState<
+ ProviderWizardInitialData | undefined
+ >(undefined);
+ const [orgWizardInitialData, setOrgWizardInitialData] = useState<
+ OrgWizardInitialData | undefined
+ >(undefined);
+
+ const openProviderWizard = (initialData?: ProviderWizardInitialData) => {
+ setOrgWizardInitialData(undefined);
+ setProviderWizardInitialData(initialData);
+ setIsProviderWizardOpen(true);
+ };
+
+ const openOrganizationWizard = (initialData: OrgWizardInitialData) => {
+ setProviderWizardInitialData(undefined);
+ setOrgWizardInitialData(initialData);
+ setIsProviderWizardOpen(true);
+ };
+
+ const handleWizardOpenChange = (open: boolean) => {
+ setIsProviderWizardOpen(open);
+
+ if (!open) {
+ setProviderWizardInitialData(undefined);
+ setOrgWizardInitialData(undefined);
+ }
+ };
+
+ return (
+ <>
+
+
+ openProviderWizard()} />
+ >
+ }
+ />
+
+
+ >
+ );
+}
diff --git a/ui/components/providers/providers-filters.tsx b/ui/components/providers/providers-filters.tsx
index c5eebad74e..d50c90f291 100644
--- a/ui/components/providers/providers-filters.tsx
+++ b/ui/components/providers/providers-filters.tsx
@@ -125,7 +125,10 @@ export const ProvidersFilters = ({
placeholder={`All ${filter.labelCheckboxGroup}`}
/>
-
+
Select All
{filter.values.map((value) => {
diff --git a/ui/components/providers/table/column-providers.tsx b/ui/components/providers/table/column-providers.tsx
index 83c9a80589..1203234a31 100644
--- a/ui/components/providers/table/column-providers.tsx
+++ b/ui/components/providers/table/column-providers.tsx
@@ -9,6 +9,10 @@ import {
ShieldOff,
} from "lucide-react";
+import type {
+ OrgWizardInitialData,
+ ProviderWizardInitialData,
+} from "@/components/providers/wizard/types";
import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { DateWithTime, EntityInfo } from "@/components/ui/entities";
@@ -108,6 +112,8 @@ export function getColumnProviders(
rowSelection: RowSelectionState,
testableProviderIds: string[],
onClearSelection: () => void,
+ onOpenProviderWizard: (initialData?: ProviderWizardInitialData) => void,
+ onOpenOrganizationWizard: (initialData: OrgWizardInitialData) => void,
): ColumnDef[] {
return [
{
@@ -320,6 +326,8 @@ export function getColumnProviders(
isRowSelected={row.getIsSelected()}
testableProviderIds={testableProviderIds}
onClearSelection={onClearSelection}
+ onOpenProviderWizard={onOpenProviderWizard}
+ onOpenOrganizationWizard={onOpenOrganizationWizard}
/>
);
},
diff --git a/ui/components/providers/table/data-table-row-actions.test.tsx b/ui/components/providers/table/data-table-row-actions.test.tsx
index 21feaf7e4e..d72874af31 100644
--- a/ui/components/providers/table/data-table-row-actions.test.tsx
+++ b/ui/components/providers/table/data-table-row-actions.test.tsx
@@ -3,9 +3,11 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
+import { ORG_SETUP_PHASE, ORG_WIZARD_STEP } from "@/types/organizations";
import {
PROVIDERS_GROUP_KIND,
PROVIDERS_ROW_TYPE,
+ ProvidersTableRow,
} from "@/types/providers-table";
const checkConnectionProviderMock = vi.hoisted(() => vi.fn());
@@ -18,10 +20,6 @@ vi.mock("@/actions/providers/providers", () => ({
checkConnectionProvider: checkConnectionProviderMock,
}));
-vi.mock("@/components/providers/wizard", () => ({
- ProviderWizardModal: () => null,
-}));
-
vi.mock("../forms/delete-form", () => ({
DeleteForm: () => null,
}));
@@ -44,7 +42,7 @@ vi.mock("@/lib/provider-helpers", () => ({
import { DataTableRowActions } from "./data-table-row-actions";
-const createRow = () =>
+const createRow = (hasSecret = false) =>
({
original: {
id: "provider-1",
@@ -74,7 +72,7 @@ const createRow = () =>
},
relationships: {
secret: {
- data: null,
+ data: hasSecret ? { id: "secret-1", type: "secrets" } : null,
},
provider_groups: {
meta: {
@@ -85,7 +83,7 @@ const createRow = () =>
},
groupNames: [],
},
- }) as Row;
+ }) as unknown as Row;
const createOrgRow = () =>
({
@@ -117,7 +115,7 @@ const createOrgRow = () =>
},
],
},
- }) as Row;
+ }) as unknown as Row;
const createOuRow = () =>
({
@@ -142,19 +140,21 @@ const createOuRow = () =>
},
],
},
- }) as Row;
+ }) as unknown as Row;
describe("DataTableRowActions", () => {
- it("renders the exact phase 1 menu actions for provider rows", async () => {
+ it("renders Add Credentials for provider rows without credentials", async () => {
// Given
const user = userEvent.setup();
render(
,
);
@@ -163,9 +163,32 @@ describe("DataTableRowActions", () => {
// Then
expect(screen.getByText("Edit Provider Alias")).toBeInTheDocument();
- expect(screen.getByText("Update Credentials")).toBeInTheDocument();
+ expect(screen.getByText("Add Credentials")).toBeInTheDocument();
expect(screen.getByText("Test Connection")).toBeInTheDocument();
expect(screen.getByText("Delete Provider")).toBeInTheDocument();
+ expect(screen.queryByText("Update Credentials")).not.toBeInTheDocument();
+ });
+
+ it("renders Update Credentials for provider rows with credentials", async () => {
+ // Given
+ const user = userEvent.setup();
+ render(
+ ,
+ );
+
+ // When
+ await user.click(screen.getByRole("button"));
+
+ // Then
+ expect(screen.getByText("Update Credentials")).toBeInTheDocument();
expect(screen.queryByText("Add Credentials")).not.toBeInTheDocument();
});
@@ -178,6 +201,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -199,6 +224,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -220,6 +247,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -238,6 +267,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={["provider-child-1", "provider-standalone"]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -257,6 +288,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={["provider-ou-child-1", "provider-standalone"]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -276,6 +309,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -286,4 +321,68 @@ describe("DataTableRowActions", () => {
).not.toBeInTheDocument();
expect(screen.queryByText("Update Credentials")).not.toBeInTheDocument();
});
+
+ it("opens the shared provider wizard when provider credentials action is selected", async () => {
+ // Given
+ const user = userEvent.setup();
+ const onOpenProviderWizard = vi.fn();
+
+ render(
+ ,
+ );
+
+ // When
+ await user.click(screen.getByRole("button"));
+ await user.click(screen.getByText("Update Credentials"));
+
+ // Then
+ expect(onOpenProviderWizard).toHaveBeenCalledWith({
+ providerId: "provider-1",
+ providerType: "aws",
+ providerUid: "111111111111",
+ providerAlias: "AWS App Account",
+ secretId: "secret-1",
+ mode: "update",
+ });
+ });
+
+ it("opens the shared organization wizard when org credentials action is selected", async () => {
+ // Given
+ const user = userEvent.setup();
+ const onOpenOrganizationWizard = vi.fn();
+
+ render(
+ ,
+ );
+
+ // When
+ await user.click(screen.getByRole("button"));
+ await user.click(screen.getByText("Update Credentials"));
+
+ // Then
+ expect(onOpenOrganizationWizard).toHaveBeenCalledWith({
+ organizationId: "org-1",
+ organizationName: "My AWS Organization",
+ externalId: "o-abc123def4",
+ targetStep: ORG_WIZARD_STEP.SETUP,
+ targetPhase: ORG_SETUP_PHASE.ACCESS,
+ intent: "edit-credentials",
+ });
+ });
});
diff --git a/ui/components/providers/table/data-table-row-actions.tsx b/ui/components/providers/table/data-table-row-actions.tsx
index 65d7901f8e..9b531f272a 100644
--- a/ui/components/providers/table/data-table-row-actions.tsx
+++ b/ui/components/providers/table/data-table-row-actions.tsx
@@ -6,10 +6,10 @@ import { useState } from "react";
import { updateOrganizationName } from "@/actions/organizations/organizations";
import { updateProvider } from "@/actions/providers";
-import { ProviderWizardModal } from "@/components/providers/wizard";
import {
ORG_WIZARD_INTENT,
OrgWizardInitialData,
+ ProviderWizardInitialData,
} from "@/components/providers/wizard/types";
import {
ActionDropdown,
@@ -44,6 +44,8 @@ interface DataTableRowActionsProps {
testableProviderIds: string[];
/** Callback to clear the row selection after bulk operation */
onClearSelection: () => void;
+ onOpenProviderWizard: (initialData?: ProviderWizardInitialData) => void;
+ onOpenOrganizationWizard: (initialData: OrgWizardInitialData) => void;
}
function collectTestableChildProviderIds(rows: ProvidersTableRow[]): string[] {
@@ -69,6 +71,7 @@ interface OrgGroupDropdownActionsProps {
onClearSelection: () => void;
onBulkTest: (ids: string[]) => Promise;
onTestChildConnections: () => Promise;
+ onOpenOrganizationWizard: (initialData: OrgWizardInitialData) => void;
}
function OrgGroupDropdownActions({
@@ -80,12 +83,10 @@ function OrgGroupDropdownActions({
onClearSelection,
onBulkTest,
onTestChildConnections,
+ onOpenOrganizationWizard,
}: OrgGroupDropdownActionsProps) {
const [isDeleteOrgOpen, setIsDeleteOrgOpen] = useState(false);
const [isEditNameOpen, setIsEditNameOpen] = useState(false);
- const [isOrgWizardOpen, setIsOrgWizardOpen] = useState(false);
- const [orgWizardData, setOrgWizardData] =
- useState(null);
const isOrgKind = rowData.groupKind === PROVIDERS_GROUP_KIND.ORGANIZATION;
const testIds = hasSelection ? testableProviderIds : childTestableIds;
@@ -97,7 +98,7 @@ function OrgGroupDropdownActions({
targetPhase: OrgWizardInitialData["targetPhase"],
intent?: OrgWizardInitialData["intent"],
) => {
- setOrgWizardData({
+ onOpenOrganizationWizard({
organizationId: rowData.id,
organizationName: rowData.name,
externalId: rowData.externalId ?? "",
@@ -105,33 +106,25 @@ function OrgGroupDropdownActions({
targetPhase,
intent,
});
- setIsOrgWizardOpen(true);
};
return (
<>
{isOrgKind && (
- <>
-
- updateOrganizationName(rowData.id, name)}
- />
-
-
+ updateOrganizationName(rowData.id, name)}
/>
- >
+
)}
);
}
@@ -369,21 +364,6 @@ export function DataTableRowActions({
)}
-
-
}
- label="Update Credentials"
- onSelect={() => setIsWizardOpen(true)}
+ label={hasSecret ? "Update Credentials" : "Add Credentials"}
+ onSelect={() =>
+ onOpenProviderWizard({
+ providerId,
+ providerType,
+ providerUid,
+ providerAlias,
+ secretId: providerSecretId,
+ mode: providerSecretId
+ ? PROVIDER_WIZARD_MODE.UPDATE
+ : PROVIDER_WIZARD_MODE.ADD,
+ })
+ }
/>
}
diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx
index 9cb55dfb87..187c1e0251 100644
--- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx
+++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx
@@ -9,8 +9,19 @@ import {
PROVIDER_WIZARD_STEP,
} from "@/types/provider-wizard";
+import type { ProviderWizardInitialData } from "../types";
import { useProviderWizardController } from "./use-provider-wizard-controller";
+const { refreshMock } = vi.hoisted(() => ({
+ refreshMock: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({
+ refresh: refreshMock,
+ }),
+}));
+
vi.mock("next-auth/react", () => ({
useSession: () => ({
data: null,
@@ -21,12 +32,33 @@ vi.mock("next-auth/react", () => ({
describe("useProviderWizardController", () => {
beforeEach(() => {
vi.useRealTimers();
+ vi.clearAllMocks();
sessionStorage.clear();
localStorage.clear();
useProviderWizardStore.getState().reset();
useOrgSetupStore.getState().reset();
});
+ it("refreshes providers data when the wizard closes", () => {
+ // Given
+ const onOpenChange = vi.fn();
+ const { result } = renderHook(() =>
+ useProviderWizardController({
+ open: true,
+ onOpenChange,
+ }),
+ );
+
+ // When
+ act(() => {
+ result.current.handleClose();
+ });
+
+ // Then
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ expect(refreshMock).toHaveBeenCalledTimes(1);
+ });
+
it("hydrates update mode when initial data is provided", async () => {
// Given
const onOpenChange = vi.fn();
@@ -121,7 +153,7 @@ describe("useProviderWizardController", () => {
expect(onOpenChange).not.toHaveBeenCalled();
});
- it("closes the modal after a successful connection test in update mode", async () => {
+ it("moves to launch step after a successful connection test in update mode", async () => {
// Given
const onOpenChange = vi.fn();
const { result } = renderHook(() =>
@@ -149,8 +181,9 @@ describe("useProviderWizardController", () => {
result.current.handleTestSuccess();
});
- // Then — update mode should close the modal, not advance to launch
- expect(onOpenChange).toHaveBeenCalledWith(false);
+ // Then
+ expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.LAUNCH);
+ expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not override launch footer config in the controller", () => {
@@ -215,14 +248,7 @@ describe("useProviderWizardController", () => {
initialData,
}: {
open: boolean;
- initialData?: {
- providerId: string;
- providerType: "gcp";
- providerUid: string;
- providerAlias: string;
- secretId: string | null;
- mode: "add" | "update";
- };
+ initialData?: ProviderWizardInitialData;
}) =>
useProviderWizardController({
open,
diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts
index 111427131e..21fce019aa 100644
--- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts
+++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts
@@ -1,5 +1,6 @@
"use client";
+import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls";
@@ -57,6 +58,7 @@ export function useProviderWizardController({
initialData,
orgInitialData,
}: UseProviderWizardControllerProps) {
+ const router = useRouter();
const initialProviderId = initialData?.providerId ?? null;
const initialProviderType = initialData?.providerType ?? null;
const initialProviderUid = initialData?.providerUid ?? null;
@@ -183,6 +185,7 @@ export function useProviderWizardController({
setProviderTypeHint(null);
setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS);
onOpenChange(false);
+ router.refresh();
};
const handleDialogOpenChange = (nextOpen: boolean) => {
@@ -194,10 +197,6 @@ export function useProviderWizardController({
};
const handleTestSuccess = () => {
- if (mode === PROVIDER_WIZARD_MODE.UPDATE) {
- handleClose();
- return;
- }
setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
};
diff --git a/ui/components/resources/resource-details-sheet.test.ts b/ui/components/resources/resource-details-sheet.test.ts
new file mode 100644
index 0000000000..9f630ee352
--- /dev/null
+++ b/ui/components/resources/resource-details-sheet.test.ts
@@ -0,0 +1,16 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("resource details sheet", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "resource-details-sheet.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("forces a remount when switching resources so local drawer state resets without effects", () => {
+ expect(source).toContain("key={resource.id}");
+ expect(source).toContain("resourceDetails={resource}");
+ });
+});
diff --git a/ui/components/resources/resource-details-sheet.tsx b/ui/components/resources/resource-details-sheet.tsx
index 1581f36520..1ae3ec0e74 100644
--- a/ui/components/resources/resource-details-sheet.tsx
+++ b/ui/components/resources/resource-details-sheet.tsx
@@ -36,7 +36,9 @@ export const ResourceDetailsSheet = ({
Close
- {open && }
+ {open && (
+
+ )}
);
diff --git a/ui/components/resources/skeleton/skeleton-finding-details.tsx b/ui/components/resources/skeleton/skeleton-finding-details.tsx
deleted file mode 100644
index 062a5fb7c7..0000000000
--- a/ui/components/resources/skeleton/skeleton-finding-details.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from "react";
-
-export const SkeletonFindingDetails = () => {
- return (
-
- {/* Header */}
-
-
- {/* Metadata Section */}
-
- {Array.from({ length: 6 }).map((_, index) => (
-
- ))}
-
-
- {/* InfoField Blocks */}
- {Array.from({ length: 3 }).map((_, index) => (
-
- ))}
-
- {/* Risk and Description Sections */}
-
-
-
-
-
-
-
-
- {/* Additional Resources */}
-
-
- {/* Categories */}
-
-
- {/* Provider Info Section */}
-
-
- );
-};
diff --git a/ui/components/resources/skeleton/skeleton-finding-summary.tsx b/ui/components/resources/skeleton/skeleton-finding-summary.tsx
deleted file mode 100644
index dde0fee56d..0000000000
--- a/ui/components/resources/skeleton/skeleton-finding-summary.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from "react";
-
-export const SkeletonFindingSummary = () => {
- return (
-
- );
-};
diff --git a/ui/components/resources/table/index.ts b/ui/components/resources/table/index.ts
index a03bacf648..65e520c5ce 100644
--- a/ui/components/resources/table/index.ts
+++ b/ui/components/resources/table/index.ts
@@ -1,5 +1,4 @@
export * from "../skeleton/skeleton-table-resources";
export * from "./column-resources";
-export * from "./resource-detail";
export * from "./resource-findings-columns";
export * from "./resources-table-with-selection";
diff --git a/ui/components/resources/table/resource-detail-content.test.ts b/ui/components/resources/table/resource-detail-content.test.ts
new file mode 100644
index 0000000000..a60c3bfb9f
--- /dev/null
+++ b/ui/components/resources/table/resource-detail-content.test.ts
@@ -0,0 +1,29 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("resource detail content", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "resource-detail-content.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("renders the new finding detail drawer flow instead of the legacy finding detail component", () => {
+ expect(source).toContain("FindingDetailDrawer");
+ expect(source).not.toContain("FindingDetail findingDetails");
+ });
+
+ it("loads the drawer bootstrap data through a single shared resource action", () => {
+ expect(source).toContain("useResourceDrawerBootstrap");
+ expect(source).not.toContain("getResourceDrawerData");
+ expect(source).not.toContain("listOrganizationsSafe");
+ expect(source).not.toContain("getResourceById");
+ expect(source).not.toContain("getLatestFindings");
+ });
+
+ it("does not import useEffect directly and relies on hooks/keyed remounts instead", () => {
+ expect(source).not.toContain("useEffect");
+ expect(source).not.toContain("useEffect(");
+ });
+});
diff --git a/ui/components/resources/table/resource-detail-content.tsx b/ui/components/resources/table/resource-detail-content.tsx
index ac1800acc6..6492f7f685 100644
--- a/ui/components/resources/table/resource-detail-content.tsx
+++ b/ui/components/resources/table/resource-detail-content.tsx
@@ -8,16 +8,12 @@ import {
CornerDownRight,
ExternalLink,
Link,
- Loader2,
} from "lucide-react";
import { useRouter } from "next/navigation";
-import { useEffect, useRef, useState } from "react";
+import { useState } from "react";
-import { getFindingById, getLatestFindings } from "@/actions/findings";
-import { listOrganizationsSafe } from "@/actions/organizations/organizations";
-import { getResourceById } from "@/actions/resources";
import { FloatingMuteButton } from "@/components/findings/floating-mute-button";
-import { FindingDetail } from "@/components/findings/table/finding-detail";
+import { FindingDetailDrawer } from "@/components/findings/table";
import {
Card,
Tabs,
@@ -32,56 +28,23 @@ import {
InfoField,
InfoTooltip,
} from "@/components/shadcn/info-field/info-field";
+import { LoadingState } from "@/components/shadcn/spinner/loading-state";
import { EventsTimeline } from "@/components/shared/events-timeline/events-timeline";
import { BreadcrumbNavigation, CustomBreadcrumbItem } from "@/components/ui";
import { DateWithTime } from "@/components/ui/entities/date-with-time";
import { EntityInfo } from "@/components/ui/entities/entity-info";
import { DataTable } from "@/components/ui/table";
-import { createDict } from "@/lib";
import { getGroupLabel } from "@/lib/categories";
import { buildGitFileUrl } from "@/lib/iac-utils";
import { getRegionFlag } from "@/lib/region-flags";
-import {
- FindingProps,
- MetaDataProps,
- ProviderType,
- ResourceProps,
-} from "@/types";
-import { OrganizationResource } from "@/types/organizations";
+import { ProviderType, ResourceProps } from "@/types";
import {
getResourceFindingsColumns,
ResourceFinding,
} from "./resource-findings-columns";
-
-function useProviderOrganization(
- providerId: string,
- providerType: string,
-): OrganizationResource | null {
- const [org, setOrg] = useState(null);
- const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
-
- useEffect(() => {
- if (!isCloudEnv || providerType !== "aws") {
- setOrg(null);
- return;
- }
-
- const loadOrg = async () => {
- const response = await listOrganizationsSafe();
- const found = response.data.find((o: OrganizationResource) =>
- o.relationships?.providers?.data?.some(
- (p: { id: string }) => p.id === providerId,
- ),
- );
- setOrg(found ?? null);
- };
-
- loadOrg();
- }, [isCloudEnv, providerType, providerId]);
-
- return org;
-}
+import { useFindingDetails } from "./use-finding-details";
+import { useResourceDrawerBootstrap } from "./use-resource-drawer-bootstrap";
const renderValue = (value: string | null | undefined) => {
return value && value.trim() !== "" ? value : "-";
@@ -146,27 +109,16 @@ interface ResourceDetailContentProps {
export const ResourceDetailContent = ({
resourceDetails,
}: ResourceDetailContentProps) => {
- const [findingsData, setFindingsData] = useState([]);
- const [findingsMetadata, setFindingsMetadata] =
- useState(null);
- const [resourceTags, setResourceTags] = useState>({});
- const [findingsLoading, setFindingsLoading] = useState(true);
- const [hasInitiallyLoaded, setHasInitiallyLoaded] = useState(false);
const [findingsReloadNonce, setFindingsReloadNonce] = useState(0);
const [selectedFindingId, setSelectedFindingId] = useState(
null,
);
- const [findingDetails, setFindingDetails] = useState(
- null,
- );
- const [findingDetailLoading, setFindingDetailLoading] = useState(false);
const [rowSelection, setRowSelection] = useState({});
const [activeTab, setActiveTab] = useState("findings");
const [metadataCopied, setMetadataCopied] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [searchQuery, setSearchQuery] = useState("");
- const findingFetchRef = useRef(null);
const router = useRouter();
const resource = resourceDetails;
@@ -174,22 +126,29 @@ export const ResourceDetailContent = ({
const attributes = resource.attributes;
const providerData = resource.relationships.provider.data.attributes;
const providerId = resource.relationships.provider.data.id;
- const providerOrg = useProviderOrganization(
+ const {
+ findingsData,
+ findingsMetadata,
+ findingsLoading,
+ hasInitiallyLoaded,
+ providerOrg,
+ resourceTags,
+ } = useResourceDrawerBootstrap({
+ resourceId,
+ resourceUid: attributes.uid,
providerId,
- providerData.provider,
- );
-
- // Reset to overview tab when switching resources
- useEffect(() => {
- setActiveTab("findings");
- }, [resourceId]);
-
- // Cleanup abort controller on unmount
- useEffect(() => {
- return () => {
- findingFetchRef.current?.abort();
- };
- }, []);
+ providerType: providerData.provider,
+ currentPage,
+ pageSize,
+ searchQuery,
+ findingsReloadNonce,
+ });
+ const {
+ findingDetails,
+ findingDetailLoading,
+ navigateToFinding: loadFindingDetails,
+ resetFindingDetails,
+ } = useFindingDetails();
const copyResourceUrl = () => {
const url = `${window.location.origin}/resources?resourceId=${resourceId}`;
@@ -202,119 +161,14 @@ export const ResourceDetailContent = ({
setTimeout(() => setMetadataCopied(false), 2000);
};
- // Load resource tags on mount
- useEffect(() => {
- const loadResourceTags = async () => {
- try {
- const resourceData = await getResourceById(resourceId, {
- fields: ["tags"],
- });
- if (resourceData?.data) {
- setResourceTags(resourceData.data.attributes.tags || {});
- }
- } catch (err) {
- console.error("Error loading resource tags:", err);
- setResourceTags({});
- }
- };
-
- if (resourceId) {
- loadResourceTags();
- }
- }, [resourceId]);
-
- // Load findings with server-side pagination and search
- useEffect(() => {
- const loadFindings = async () => {
- setFindingsLoading(true);
-
- try {
- const findingsResponse = await getLatestFindings({
- page: currentPage,
- pageSize,
- query: searchQuery,
- sort: "severity,-inserted_at",
- filters: {
- "filter[resource_uid]": attributes.uid,
- "filter[status]": "FAIL",
- },
- });
-
- if (findingsResponse?.data) {
- setFindingsMetadata(findingsResponse.meta || null);
- setFindingsData(findingsResponse.data as ResourceFinding[]);
- } else {
- setFindingsData([]);
- setFindingsMetadata(null);
- }
- } catch (err) {
- console.error("Error loading findings:", err);
- setFindingsData([]);
- setFindingsMetadata(null);
- } finally {
- setFindingsLoading(false);
- setHasInitiallyLoaded(true);
- }
- };
-
- if (attributes.uid) {
- loadFindings();
- }
- }, [attributes.uid, currentPage, pageSize, searchQuery, findingsReloadNonce]);
-
const navigateToFinding = async (findingId: string) => {
- if (findingFetchRef.current) {
- findingFetchRef.current.abort();
- }
- findingFetchRef.current = new AbortController();
-
setSelectedFindingId(findingId);
- setFindingDetailLoading(true);
-
- try {
- const findingData = await getFindingById(
- findingId,
- "resources,scan.provider",
- );
-
- if (findingFetchRef.current?.signal.aborted) {
- return;
- }
-
- if (findingData?.data) {
- const resourceDict = createDict("resources", findingData);
- const scanDict = createDict("scans", findingData);
- const providerDict = createDict("providers", findingData);
-
- const finding = findingData.data;
- const scan = scanDict[finding.relationships?.scan?.data?.id];
- const foundResource =
- resourceDict[finding.relationships?.resources?.data?.[0]?.id];
- const provider = providerDict[scan?.relationships?.provider?.data?.id];
-
- const expandedFinding = {
- ...finding,
- relationships: { scan, resource: foundResource, provider },
- };
-
- setFindingDetails(expandedFinding);
- }
- } catch (error) {
- if (error instanceof Error && error.name === "AbortError") {
- return;
- }
- console.error("Error fetching finding:", error);
- } finally {
- if (!findingFetchRef.current?.signal.aborted) {
- setFindingDetailLoading(false);
- }
- }
+ await loadFindingDetails(findingId);
};
const handleBackToResource = () => {
setSelectedFindingId(null);
- setFindingDetails(null);
- setFindingDetailLoading(false);
+ resetFindingDetails();
};
const handleMuteComplete = (_findingIds?: string[]) => {
@@ -332,11 +186,6 @@ export const ResourceDetailContent = ({
(f) => !f.attributes.muted,
).length;
- // Reset selection when page changes
- useEffect(() => {
- setRowSelection({});
- }, [currentPage, pageSize]);
-
const totalFindings = findingsMetadata?.pagination?.count || 0;
const getRowCanSelect = (row: Row): boolean =>
@@ -396,14 +245,16 @@ export const ResourceDetailContent = ({
/>
{findingDetailLoading ? (
-
-
-
- Loading finding details...
-
-
+
) : (
- findingDetails &&
+ findingDetails && (
+
+ )
)}
);
@@ -543,12 +394,7 @@ export const ResourceDetailContent = ({
{findingsLoading && !hasInitiallyLoaded ? (
-
-
-
- Loading findings...
-
-
+
) : (
<>
{
+ setRowSelection({});
setSearchQuery(value);
setCurrentPage(1);
}}
controlledPage={currentPage}
controlledPageSize={pageSize}
- onPageChange={setCurrentPage}
- onPageSizeChange={setPageSize}
+ onPageChange={(page) => {
+ setRowSelection({});
+ setCurrentPage(page);
+ }}
+ onPageSizeChange={(size) => {
+ setRowSelection({});
+ setCurrentPage(1);
+ setPageSize(size);
+ }}
isLoading={findingsLoading}
/>
{selectedFindingIds.length > 0 && (
@@ -655,10 +509,12 @@ export const ResourceDetailContent = ({
-
+ {activeTab === "events" && (
+
+ )}
diff --git a/ui/components/resources/table/resource-detail.tsx b/ui/components/resources/table/resource-detail.tsx
deleted file mode 100644
index 88cc57095c..0000000000
--- a/ui/components/resources/table/resource-detail.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-"use client";
-
-import { X } from "lucide-react";
-import type { ReactNode } from "react";
-import { useState } from "react";
-
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from "@/components/shadcn";
-import { ResourceProps } from "@/types";
-
-import { ResourceDetailContent } from "./resource-detail-content";
-
-interface ResourceDetailProps {
- resourceDetails: ResourceProps;
- trigger?: ReactNode;
- open?: boolean;
- defaultOpen?: boolean;
- onOpenChange?: (open: boolean) => void;
-}
-
-/**
- * Lightweight wrapper component for resource details.
- *
- * When used with a trigger (table rows), this component only renders the Drawer shell
- * and trigger. The heavy ResourceDetailContent is only mounted when the drawer is open,
- * preventing unnecessary state initialization and data fetching for closed drawers.
- *
- * When used without a trigger (inline mode from ResourceDetailsSheet), it renders
- * the content directly since it's already visible.
- */
-export const ResourceDetail = ({
- resourceDetails,
- trigger,
- open: controlledOpen,
- defaultOpen = false,
- onOpenChange,
-}: ResourceDetailProps) => {
- // Track internal open state for uncontrolled drawer (when using trigger)
- const [internalOpen, setInternalOpen] = useState(defaultOpen);
-
- // Determine actual open state
- const isOpen = controlledOpen ?? internalOpen;
-
- // Handle open state changes
- const handleOpenChange = (newOpen: boolean) => {
- setInternalOpen(newOpen);
- onOpenChange?.(newOpen);
- };
-
- // If no trigger, render content directly (inline mode for ResourceDetailsSheet)
- if (!trigger) {
- return ;
- }
-
- // With trigger, wrap in Drawer - content only mounts when open (lazy loading)
- return (
-
- {trigger}
-
-
- Resource Details
- View the resource details
-
-
-
- Close
-
- {/* Content only renders when drawer is open - this is the key optimization */}
- {isOpen && }
-
-
- );
-};
diff --git a/ui/components/resources/table/use-finding-details.ts b/ui/components/resources/table/use-finding-details.ts
new file mode 100644
index 0000000000..b95770a6a6
--- /dev/null
+++ b/ui/components/resources/table/use-finding-details.ts
@@ -0,0 +1,72 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+
+import { getFindingById } from "@/actions/findings";
+import { expandFindingWithRelationships } from "@/lib/finding-detail";
+import { FindingProps } from "@/types";
+
+interface UseFindingDetailsReturn {
+ findingDetails: FindingProps | null;
+ findingDetailLoading: boolean;
+ navigateToFinding: (findingId: string) => Promise;
+ resetFindingDetails: () => void;
+}
+
+export function useFindingDetails(): UseFindingDetailsReturn {
+ const [findingDetails, setFindingDetails] = useState(
+ null,
+ );
+ const [findingDetailLoading, setFindingDetailLoading] = useState(false);
+ const findingFetchRef = useRef(null);
+
+ useEffect(() => {
+ return () => {
+ findingFetchRef.current?.abort();
+ };
+ }, []);
+
+ const navigateToFinding = async (findingId: string) => {
+ if (findingFetchRef.current) {
+ findingFetchRef.current.abort();
+ }
+ findingFetchRef.current = new AbortController();
+ setFindingDetailLoading(true);
+
+ try {
+ const findingData = await getFindingById(
+ findingId,
+ "resources,scan.provider",
+ );
+
+ if (findingFetchRef.current?.signal.aborted) {
+ return;
+ }
+
+ if (findingData?.data) {
+ setFindingDetails(expandFindingWithRelationships(findingData));
+ }
+ } catch (error) {
+ if (error instanceof Error && error.name === "AbortError") {
+ return;
+ }
+ console.error("Error fetching finding:", error);
+ } finally {
+ if (!findingFetchRef.current?.signal.aborted) {
+ setFindingDetailLoading(false);
+ }
+ }
+ };
+
+ const resetFindingDetails = () => {
+ setFindingDetails(null);
+ setFindingDetailLoading(false);
+ };
+
+ return {
+ findingDetails,
+ findingDetailLoading,
+ navigateToFinding,
+ resetFindingDetails,
+ };
+}
diff --git a/ui/components/resources/table/use-resource-drawer-bootstrap.ts b/ui/components/resources/table/use-resource-drawer-bootstrap.ts
new file mode 100644
index 0000000000..7508a70626
--- /dev/null
+++ b/ui/components/resources/table/use-resource-drawer-bootstrap.ts
@@ -0,0 +1,115 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+import { getResourceDrawerData } from "@/actions/resources";
+import { MetaDataProps } from "@/types";
+import { OrganizationResource } from "@/types/organizations";
+
+import { ResourceFinding } from "./resource-findings-columns";
+
+interface UseResourceDrawerBootstrapOptions {
+ resourceId: string;
+ resourceUid: string;
+ providerId: string;
+ providerType: string;
+ currentPage: number;
+ pageSize: number;
+ searchQuery: string;
+ findingsReloadNonce: number;
+}
+
+interface UseResourceDrawerBootstrapReturn {
+ findingsData: ResourceFinding[];
+ findingsMetadata: MetaDataProps | null;
+ findingsLoading: boolean;
+ hasInitiallyLoaded: boolean;
+ providerOrg: OrganizationResource | null;
+ resourceTags: Record;
+}
+
+export function useResourceDrawerBootstrap({
+ resourceId,
+ resourceUid,
+ providerId,
+ providerType,
+ currentPage,
+ pageSize,
+ searchQuery,
+ findingsReloadNonce,
+}: UseResourceDrawerBootstrapOptions): UseResourceDrawerBootstrapReturn {
+ const [findingsData, setFindingsData] = useState([]);
+ const [findingsMetadata, setFindingsMetadata] =
+ useState(null);
+ const [resourceTags, setResourceTags] = useState>({});
+ const [findingsLoading, setFindingsLoading] = useState(true);
+ const [hasInitiallyLoaded, setHasInitiallyLoaded] = useState(false);
+ const [providerOrg, setProviderOrg] = useState(
+ null,
+ );
+
+ useEffect(() => {
+ let cancelled = false;
+
+ const loadResourceDrawerData = async () => {
+ setFindingsLoading(true);
+
+ try {
+ const drawerData = await getResourceDrawerData({
+ resourceId,
+ resourceUid,
+ providerId,
+ providerType,
+ page: currentPage,
+ pageSize,
+ query: searchQuery,
+ });
+
+ if (cancelled) return;
+
+ setResourceTags(drawerData.resourceTags);
+ setProviderOrg(drawerData.providerOrg);
+ setFindingsMetadata(drawerData.findingsMeta as MetaDataProps | null);
+ setFindingsData(drawerData.findings as ResourceFinding[]);
+ } catch (error) {
+ if (cancelled) return;
+ console.error("Error loading resource drawer data:", error);
+ setResourceTags({});
+ setProviderOrg(null);
+ setFindingsData([]);
+ setFindingsMetadata(null);
+ } finally {
+ if (!cancelled) {
+ setFindingsLoading(false);
+ setHasInitiallyLoaded(true);
+ }
+ }
+ };
+
+ if (resourceUid) {
+ loadResourceDrawerData();
+ }
+
+ return () => {
+ cancelled = true;
+ };
+ }, [
+ currentPage,
+ findingsReloadNonce,
+ pageSize,
+ providerId,
+ providerType,
+ resourceId,
+ resourceUid,
+ searchQuery,
+ ]);
+
+ return {
+ findingsData,
+ findingsMetadata,
+ findingsLoading,
+ hasInitiallyLoaded,
+ providerOrg,
+ resourceTags,
+ };
+}
diff --git a/ui/components/scans/table/scans/column-get-scans.test.ts b/ui/components/scans/table/scans/column-get-scans.test.ts
new file mode 100644
index 0000000000..88757c5d98
--- /dev/null
+++ b/ui/components/scans/table/scans/column-get-scans.test.ts
@@ -0,0 +1,22 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("column-get-scans", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "column-get-scans.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("links scan findings to the historical finding-groups filters", () => {
+ expect(source).toContain("filter[scan]=");
+ expect(source).toContain("filter[inserted_at]=");
+ expect(source).not.toContain("filter[scan__in]");
+ });
+
+ it("links the findings filter against the scan's completed_at (what the backend expects)", () => {
+ expect(source).toMatch(/attributes:\s*{\s*completed_at\s*}/);
+ expect(source).toMatch(/toLocalDateString\(completed_at\)/);
+ });
+});
diff --git a/ui/components/scans/table/scans/column-get-scans.tsx b/ui/components/scans/table/scans/column-get-scans.tsx
index 5f55628b29..e9a073ac1c 100644
--- a/ui/components/scans/table/scans/column-get-scans.tsx
+++ b/ui/components/scans/table/scans/column-get-scans.tsx
@@ -8,10 +8,10 @@ import { TableLink } from "@/components/ui/custom";
import { DateWithTime, EntityInfo } from "@/components/ui/entities";
import { TriggerSheet } from "@/components/ui/sheet";
import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table";
+import { toLocalDateString } from "@/lib/date-utils";
import { ProviderType, ScanProps } from "@/types";
import { TriggerIcon } from "../../trigger-icon";
-import { DataTableDownloadDetails } from "./data-table-download-details";
import { DataTableRowActions } from "./data-table-row-actions";
import { DataTableRowDetails } from "./data-table-row-details";
@@ -97,24 +97,6 @@ export const ColumnGetScans: ColumnDef[] = [
enableSorting: false,
},
- {
- accessorKey: "started_at",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const {
- attributes: { started_at },
- } = getScanData(row);
-
- return (
-
-
-
- );
- },
- enableSorting: false,
- },
{
accessorKey: "status",
header: ({ column }) => (
@@ -141,12 +123,22 @@ export const ColumnGetScans: ColumnDef[] = [
),
cell: ({ row }) => {
- const { id } = getScanData(row);
+ const {
+ id,
+ attributes: { completed_at },
+ } = getScanData(row);
const scanState = row.original.attributes?.state;
+ // Source is `completed_at` (scan finish time) because findings are
+ // persisted when the scan ends — that's when their `inserted_at` is
+ // written. The URL key stays `filter[inserted_at]` because the findings
+ // table is partitioned by the finding's `inserted_at` date; this filter
+ // is the partition hint the backend uses to avoid scanning every
+ // partition. Names differ by design: scan.completed_at ≈ finding.inserted_at.
+ const scanDate = toLocalDateString(completed_at);
return (
);
@@ -171,24 +163,10 @@ export const ColumnGetScans: ColumnDef[] = [
},
enableSorting: false,
},
- {
- id: "download",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- return (
-
-
-
- );
- },
- enableSorting: false,
- },
{
accessorKey: "resources",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const {
@@ -202,6 +180,24 @@ export const ColumnGetScans: ColumnDef[] = [
},
enableSorting: false,
},
+ {
+ accessorKey: "started_at",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { started_at },
+ } = getScanData(row);
+
+ return (
+
+
+
+ );
+ },
+ enableSorting: false,
+ },
{
accessorKey: "scheduled_at",
header: ({ column }) => (
diff --git a/ui/components/scans/table/scans/data-table-download-details.tsx b/ui/components/scans/table/scans/data-table-download-details.tsx
deleted file mode 100644
index 4579ef3c56..0000000000
--- a/ui/components/scans/table/scans/data-table-download-details.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Row } from "@tanstack/react-table";
-import { useState } from "react";
-
-import { DownloadIconButton, useToast } from "@/components/ui";
-import { downloadScanZip } from "@/lib";
-
-interface DataTableDownloadDetailsProps {
- row: Row;
-}
-
-export function DataTableDownloadDetails({
- row,
-}: DataTableDownloadDetailsProps) {
- const { toast } = useToast();
- const [isDownloading, setIsDownloading] = useState(false);
-
- const scanId = (row.original as { id: string }).id;
- const scanState = (row.original as any).attributes?.state;
-
- const handleDownload = async () => {
- setIsDownloading(true);
- await downloadScanZip(scanId, toast);
- setIsDownloading(false);
- };
-
- return (
-
- );
-}
diff --git a/ui/components/shadcn/index.ts b/ui/components/shadcn/index.ts
index d63d487283..69ca232f67 100644
--- a/ui/components/shadcn/index.ts
+++ b/ui/components/shadcn/index.ts
@@ -11,6 +11,7 @@ export * from "./drawer";
export * from "./dropdown/dropdown";
export * from "./info-field";
export * from "./input/input";
+export * from "./progress";
export * from "./search-input/search-input";
export * from "./select/multiselect";
export * from "./select/select";
diff --git a/ui/components/shadcn/progress.tsx b/ui/components/shadcn/progress.tsx
new file mode 100644
index 0000000000..f3ba3d8ce0
--- /dev/null
+++ b/ui/components/shadcn/progress.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import * as ProgressPrimitive from "@radix-ui/react-progress";
+import { ComponentProps } from "react";
+
+import { cn } from "@/lib/utils";
+
+interface ProgressProps extends ComponentProps {
+ indicatorClassName?: string;
+}
+
+function Progress({
+ className,
+ value = 0,
+ indicatorClassName,
+ ...props
+}: ProgressProps) {
+ const normalizedValue = value ?? 0;
+
+ return (
+
+
+
+ );
+}
+
+export { Progress };
diff --git a/ui/components/shadcn/select/multiselect.test.tsx b/ui/components/shadcn/select/multiselect.test.tsx
index e87804ab4f..d5e497e8ce 100644
--- a/ui/components/shadcn/select/multiselect.test.tsx
+++ b/ui/components/shadcn/select/multiselect.test.tsx
@@ -47,6 +47,33 @@ describe("MultiSelect", () => {
expect(
within(screen.getByRole("combobox")).getByText("Production AWS"),
).toBeInTheDocument();
+ expect(
+ within(screen.getByRole("combobox")).queryByText("Select accounts"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("keeps the filter label context when a value is selected", () => {
+ render(
+ {}}>
+
+
+
+
+ FAIL
+ PASS
+
+ ,
+ );
+
+ expect(
+ within(screen.getByRole("combobox")).getByText("Status"),
+ ).toBeInTheDocument();
+ expect(
+ within(screen.getByRole("combobox")).getByText("FAIL"),
+ ).toBeInTheDocument();
+ expect(
+ within(screen.getByRole("combobox")).queryByText("All Status"),
+ ).not.toBeInTheDocument();
});
it("filters items without crashing when search is enabled", async () => {
diff --git a/ui/components/shadcn/select/multiselect.tsx b/ui/components/shadcn/select/multiselect.tsx
index 1658a3c20e..e7dca260db 100644
--- a/ui/components/shadcn/select/multiselect.tsx
+++ b/ui/components/shadcn/select/multiselect.tsx
@@ -163,6 +163,10 @@ export function MultiSelectValue({
const shouldWrap =
overflowBehavior === "wrap" ||
(overflowBehavior === "wrap-when-open" && open);
+ const selectedContextLabel =
+ placeholder && /^All\s+/i.test(placeholder) && selectedValues.size > 0
+ ? placeholder.replace(/^All\s+/i, "").trim()
+ : "";
const checkOverflow = useCallback(() => {
if (valueRef.current === null) return;
@@ -222,11 +226,16 @@ export function MultiSelectValue({
className,
)}
>
- {placeholder && (
+ {placeholder && selectedValues.size === 0 && (
{placeholder}
)}
+ {selectedContextLabel && (
+
+ {selectedContextLabel}
+
+ )}
{Array.from(selectedValues)
.filter((value) => items.has(value))
.map((value) => (
diff --git a/ui/components/shadcn/spinner/loading-state.test.tsx b/ui/components/shadcn/spinner/loading-state.test.tsx
new file mode 100644
index 0000000000..c2cddc59e7
--- /dev/null
+++ b/ui/components/shadcn/spinner/loading-state.test.tsx
@@ -0,0 +1,36 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { LoadingState } from "./loading-state";
+
+describe("LoadingState", () => {
+ it("renders a spinner with the default size", () => {
+ const { container } = render( );
+ const svg = container.querySelector("svg");
+ expect(svg).toHaveAttribute("aria-label", "Loading");
+ expect(
+ svg?.className.baseVal ?? svg?.getAttribute("class") ?? "",
+ ).toContain("size-6");
+ });
+
+ it("does not render a label when none is provided", () => {
+ render( );
+ expect(screen.queryByText(/loading/i, { selector: "span" })).toBeNull();
+ });
+
+ it("renders the label when provided", () => {
+ render( );
+ expect(screen.getByText("Loading findings...")).toBeInTheDocument();
+ });
+
+ it("forwards spinnerClassName to the Spinner so callers can override the size", () => {
+ const { container } = render( );
+ const svg = container.querySelector("svg");
+ expect(svg?.getAttribute("class") ?? "").toContain("size-5");
+ });
+
+ it("forwards className to the wrapper element", () => {
+ const { container } = render( );
+ expect(container.firstChild).toHaveClass("custom-wrapper");
+ });
+});
diff --git a/ui/components/shadcn/spinner/loading-state.tsx b/ui/components/shadcn/spinner/loading-state.tsx
new file mode 100644
index 0000000000..77355c840f
--- /dev/null
+++ b/ui/components/shadcn/spinner/loading-state.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+
+import { Spinner } from "./spinner";
+
+interface LoadingStateProps {
+ label?: string;
+ className?: string;
+ spinnerClassName?: string;
+}
+
+export function LoadingState({
+ label,
+ className,
+ spinnerClassName,
+}: LoadingStateProps) {
+ return (
+
+
+ {label && (
+ {label}
+ )}
+
+ );
+}
diff --git a/ui/components/shared/events-timeline/events-timeline.test.ts b/ui/components/shared/events-timeline/events-timeline.test.ts
new file mode 100644
index 0000000000..41354c4747
--- /dev/null
+++ b/ui/components/shared/events-timeline/events-timeline.test.ts
@@ -0,0 +1,17 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("events timeline", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "events-timeline.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("delegates resource event loading to a dedicated hook instead of using useEffect in the component", () => {
+ expect(source).toContain("useResourceEventsTimeline");
+ expect(source).not.toContain("getResourceEvents");
+ expect(source).not.toContain("useEffect(");
+ });
+});
diff --git a/ui/components/shared/events-timeline/events-timeline.tsx b/ui/components/shared/events-timeline/events-timeline.tsx
index c4f248ed51..e386e5528d 100644
--- a/ui/components/shared/events-timeline/events-timeline.tsx
+++ b/ui/components/shared/events-timeline/events-timeline.tsx
@@ -8,9 +8,8 @@ import {
Server,
Shield,
} from "lucide-react";
-import { useEffect, useState, useTransition } from "react";
+import { useState } from "react";
-import { getResourceEvents } from "@/actions/resources";
import {
Alert,
AlertDescription,
@@ -25,6 +24,8 @@ import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { cn } from "@/lib/utils";
import { ResourceEventProps } from "@/types";
+import { useResourceEventsTimeline } from "./use-resource-events-timeline";
+
interface EventsTimelineProps {
resourceId?: string;
isAwsProvider: boolean;
@@ -34,59 +35,17 @@ export const EventsTimeline = ({
resourceId,
isAwsProvider,
}: EventsTimelineProps) => {
- const [events, setEvents] = useState([]);
- const [error, setError] = useState(null);
- const [errorStatus, setErrorStatus] = useState(null);
const [expandedRows, setExpandedRows] = useState>(new Set());
const [includeReadEvents, setIncludeReadEvents] = useState(false);
- const [hasFetched, setHasFetched] = useState(false);
const [retryCount, setRetryCount] = useState(0);
- const [isPending, startTransition] = useTransition();
-
- useEffect(() => {
- if (!isAwsProvider || !resourceId) return;
-
- let cancelled = false;
-
- setError(null);
- setErrorStatus(null);
- setHasFetched(false);
-
- startTransition(async () => {
- try {
- const response = await getResourceEvents(resourceId, {
- includeReadEvents,
- });
-
- if (cancelled) return;
-
- if (!response) {
- setError("Failed to fetch events. Please try again.");
- return;
- }
-
- if (response.error) {
- setError(response.error);
- setErrorStatus(response.status || null);
- return;
- }
-
- setEvents(response.data || []);
- setExpandedRows(new Set());
- } catch (err) {
- if (cancelled) return;
- console.error("Error fetching events:", err);
- setError("An unexpected error occurred.");
- } finally {
- if (!cancelled) setHasFetched(true);
- }
+ const { events, error, errorStatus, hasFetched, isPending } =
+ useResourceEventsTimeline({
+ resourceId,
+ isAwsProvider,
+ includeReadEvents,
+ retryCount,
});
- return () => {
- cancelled = true;
- };
- }, [resourceId, includeReadEvents, isAwsProvider, retryCount]);
-
const toggleRow = (eventId: string) => {
setExpandedRows((prev) => {
const next = new Set(prev);
diff --git a/ui/components/shared/events-timeline/use-resource-events-timeline.ts b/ui/components/shared/events-timeline/use-resource-events-timeline.ts
new file mode 100644
index 0000000000..9c0717be27
--- /dev/null
+++ b/ui/components/shared/events-timeline/use-resource-events-timeline.ts
@@ -0,0 +1,85 @@
+"use client";
+
+import { useEffect, useState, useTransition } from "react";
+
+import { getResourceEvents } from "@/actions/resources";
+import { ResourceEventProps } from "@/types";
+
+interface UseResourceEventsTimelineOptions {
+ resourceId?: string;
+ isAwsProvider: boolean;
+ includeReadEvents: boolean;
+ retryCount: number;
+}
+
+interface UseResourceEventsTimelineReturn {
+ events: ResourceEventProps[];
+ error: string | null;
+ errorStatus: number | null;
+ hasFetched: boolean;
+ isPending: boolean;
+}
+
+export function useResourceEventsTimeline({
+ resourceId,
+ isAwsProvider,
+ includeReadEvents,
+ retryCount,
+}: UseResourceEventsTimelineOptions): UseResourceEventsTimelineReturn {
+ const [events, setEvents] = useState([]);
+ const [error, setError] = useState(null);
+ const [errorStatus, setErrorStatus] = useState(null);
+ const [hasFetched, setHasFetched] = useState(false);
+ const [isPending, startTransition] = useTransition();
+
+ useEffect(() => {
+ if (!isAwsProvider || !resourceId) return;
+
+ let cancelled = false;
+
+ setError(null);
+ setErrorStatus(null);
+ setHasFetched(false);
+
+ startTransition(async () => {
+ try {
+ const response = await getResourceEvents(resourceId, {
+ includeReadEvents,
+ });
+
+ if (cancelled) return;
+
+ if (!response) {
+ setError("Failed to fetch events. Please try again.");
+ return;
+ }
+
+ if (response.error) {
+ setError(response.error);
+ setErrorStatus(response.status || null);
+ return;
+ }
+
+ setEvents(response.data || []);
+ } catch (err) {
+ if (cancelled) return;
+ console.error("Error fetching events:", err);
+ setError("An unexpected error occurred.");
+ } finally {
+ if (!cancelled) setHasFetched(true);
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [resourceId, includeReadEvents, isAwsProvider, retryCount]);
+
+ return {
+ events,
+ error,
+ errorStatus,
+ hasFetched,
+ isPending,
+ };
+}
diff --git a/ui/components/shared/query-code-editor.tsx b/ui/components/shared/query-code-editor.tsx
index cc47c1f199..96997d2638 100644
--- a/ui/components/shared/query-code-editor.tsx
+++ b/ui/components/shared/query-code-editor.tsx
@@ -3,6 +3,7 @@
import {
HighlightStyle,
StreamLanguage,
+ type StringStream,
syntaxHighlighting,
} from "@codemirror/language";
import { EditorState } from "@codemirror/state";
@@ -20,12 +21,16 @@ import { type HTMLAttributes, useState } from "react";
import { Badge } from "@/components/shadcn";
import { cn } from "@/lib/utils";
-const QUERY_EDITOR_LANGUAGE = {
+export const QUERY_EDITOR_LANGUAGE = {
OPEN_CYPHER: "openCypher",
PLAIN_TEXT: "plainText",
+ SHELL: "shell",
+ HCL: "hcl",
+ BICEP: "bicep",
+ YAML: "yaml",
} as const;
-type QueryEditorLanguage =
+export type QueryEditorLanguage =
(typeof QUERY_EDITOR_LANGUAGE)[keyof typeof QUERY_EDITOR_LANGUAGE];
const OPEN_CYPHER_KEYWORDS = new Set([
@@ -98,11 +103,204 @@ const OPEN_CYPHER_FUNCTIONS = new Set([
"type",
]);
+const SHELL_KEYWORDS = new Set([
+ "if",
+ "then",
+ "else",
+ "elif",
+ "fi",
+ "for",
+ "in",
+ "do",
+ "done",
+ "while",
+ "until",
+ "case",
+ "esac",
+ "function",
+ "return",
+ "export",
+ "local",
+ "readonly",
+ "declare",
+ "typeset",
+ "unset",
+ "shift",
+ "break",
+ "continue",
+ "select",
+ "time",
+ "trap",
+]);
+
+const SHELL_COMMANDS = new Set([
+ "aws",
+ "az",
+ "gcloud",
+ "kubectl",
+ "terraform",
+ "echo",
+ "grep",
+ "sed",
+ "awk",
+ "curl",
+ "wget",
+ "chmod",
+ "chown",
+ "mkdir",
+ "rm",
+ "cp",
+ "mv",
+ "cat",
+ "ls",
+]);
+
+const HCL_KEYWORDS = new Set([
+ "resource",
+ "data",
+ "variable",
+ "output",
+ "locals",
+ "module",
+ "provider",
+ "terraform",
+ "backend",
+ "required_providers",
+ "dynamic",
+ "for_each",
+ "count",
+ "depends_on",
+ "lifecycle",
+ "provisioner",
+ "connection",
+]);
+
+const HCL_FUNCTIONS = new Set([
+ "lookup",
+ "merge",
+ "join",
+ "split",
+ "length",
+ "element",
+ "concat",
+ "format",
+ "replace",
+ "regex",
+ "tolist",
+ "tomap",
+ "toset",
+ "try",
+ "can",
+ "file",
+ "templatefile",
+ "jsonencode",
+ "jsondecode",
+ "yamlencode",
+ "yamldecode",
+ "base64encode",
+ "base64decode",
+ "md5",
+ "sha256",
+ "cidrsubnet",
+ "cidrhost",
+]);
+
+const BICEP_KEYWORDS = new Set([
+ "resource",
+ "module",
+ "param",
+ "var",
+ "output",
+ "type",
+ "metadata",
+ "import",
+ "using",
+ "extension",
+ "targetScope",
+ "existing",
+ "if",
+ "for",
+ "in",
+ "true",
+ "false",
+ "null",
+]);
+
+const BICEP_DECORATORS = new Set([
+ "description",
+ "secure",
+ "minLength",
+ "maxLength",
+ "minValue",
+ "maxValue",
+ "allowed",
+ "metadata",
+ "batchSize",
+ "sys",
+]);
+
+const BICEP_FUNCTIONS = new Set([
+ "concat",
+ "format",
+ "toLower",
+ "toUpper",
+ "substring",
+ "replace",
+ "split",
+ "join",
+ "length",
+ "contains",
+ "empty",
+ "first",
+ "last",
+ "indexOf",
+ "array",
+ "union",
+ "intersection",
+ "resourceId",
+ "subscriptionResourceId",
+ "tenantResourceId",
+ "reference",
+ "listKeys",
+ "listAccountSas",
+ "uniqueString",
+ "guid",
+ "base64",
+ "uri",
+ "environment",
+ "subscription",
+ "resourceGroup",
+ "tenant",
+]);
+
interface OpenCypherParserState {
inBlockComment: boolean;
inString: "'" | '"' | null;
}
+interface ShellParserState {
+ inString: "'" | '"' | null;
+}
+
+interface HclParserState {
+ inBlockComment: boolean;
+ inString: '"' | null;
+ expectBlockType: boolean;
+ heredocTerminator: string | null;
+}
+
+interface BicepParserState {
+ inBlockComment: boolean;
+ inString: "'" | null;
+ expectResourceType: boolean;
+}
+
+interface YamlParserState {
+ inString: "'" | '"' | null;
+ inBlockScalar: boolean;
+ blockScalarIndent: number;
+}
+
const openCypherLanguage = StreamLanguage.define({
startState() {
return {
@@ -216,6 +414,595 @@ const openCypherLanguage = StreamLanguage.define({
},
});
+const shellLanguage = StreamLanguage.define({
+ startState() {
+ return {
+ inString: null,
+ };
+ },
+ token(stream, state) {
+ if (state.inString) {
+ let escaped = false;
+
+ while (!stream.eol()) {
+ const next = stream.next();
+
+ if (escaped) {
+ escaped = false;
+ continue;
+ }
+
+ if (next === "\\" && state.inString === '"') {
+ escaped = true;
+ continue;
+ }
+
+ if (next === state.inString) {
+ state.inString = null;
+ break;
+ }
+ }
+
+ if (stream.eol()) {
+ state.inString = null;
+ }
+
+ return "string";
+ }
+
+ if (stream.eatSpace()) {
+ return null;
+ }
+
+ if (stream.peek() === "#") {
+ stream.skipToEnd();
+ return "comment";
+ }
+
+ if (stream.match(/\$\([^)]+\)/)) {
+ return "variableName";
+ }
+
+ if (stream.match(/\$\{[A-Za-z_][\w]*\}/)) {
+ return "variableName";
+ }
+
+ if (stream.match(/\$[A-Za-z_][\w]*/)) {
+ return "variableName";
+ }
+
+ const quote = stream.peek();
+ if (quote === "'" || quote === '"') {
+ state.inString = quote;
+ stream.next();
+ return "string";
+ }
+
+ if (stream.match(/--[A-Za-z0-9][\w-]*/)) {
+ return "operator";
+ }
+
+ if (stream.match(/-[A-Za-z0-9]+/)) {
+ return "operator";
+ }
+
+ if (stream.match(/\|\||&&|>>|[|><;]/)) {
+ return "operator";
+ }
+
+ if (stream.match(/\d+(?:\.\d+)?/)) {
+ return "number";
+ }
+
+ if (stream.match(/[A-Za-z_][\w-]*/)) {
+ const currentValue = stream.current();
+ const normalizedValue = currentValue.toLowerCase();
+
+ if (SHELL_KEYWORDS.has(normalizedValue)) {
+ return "keyword";
+ }
+
+ if (SHELL_COMMANDS.has(normalizedValue)) {
+ return "function";
+ }
+
+ return "variableName";
+ }
+
+ stream.next();
+ return null;
+ },
+});
+
+const hclLanguage = StreamLanguage.define({
+ startState() {
+ return {
+ inBlockComment: false,
+ inString: null,
+ expectBlockType: false,
+ heredocTerminator: null,
+ };
+ },
+ token(stream, state) {
+ if (state.heredocTerminator) {
+ // Match the closing terminator on its own line, including indented <<-EOF forms.
+ if (
+ stream.sol() &&
+ stream.match(new RegExp(`^\\s*${state.heredocTerminator}\\s*$`))
+ ) {
+ state.heredocTerminator = null;
+ return "keyword";
+ }
+
+ stream.skipToEnd();
+ return "string";
+ }
+
+ if (state.inBlockComment) {
+ while (!stream.eol()) {
+ if (stream.match("*/")) {
+ state.inBlockComment = false;
+ break;
+ }
+
+ stream.next();
+ }
+
+ return "comment";
+ }
+
+ if (state.inString) {
+ let escaped = false;
+
+ while (!stream.eol()) {
+ const next = stream.next();
+
+ if (escaped) {
+ escaped = false;
+ continue;
+ }
+
+ if (next === "\\") {
+ escaped = true;
+ continue;
+ }
+
+ if (next === state.inString) {
+ state.inString = null;
+ break;
+ }
+ }
+
+ if (stream.eol()) {
+ state.inString = null;
+ }
+
+ return "string";
+ }
+
+ if (stream.eatSpace()) {
+ return null;
+ }
+
+ if (stream.match("#") || stream.match("//")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+
+ if (stream.match("/*")) {
+ state.inBlockComment = true;
+ return "comment";
+ }
+
+ if (stream.match(/\$\{[^}]+\}/)) {
+ return "variableName";
+ }
+
+ if (stream.peek() === '"') {
+ if (state.expectBlockType) {
+ state.expectBlockType = false;
+ stream.next(); // opening "
+ while (!stream.eol()) {
+ const ch = stream.next();
+ if (ch === "\\") {
+ stream.next(); // skip escaped char
+ } else if (ch === '"') {
+ break;
+ }
+ }
+ return "typeName";
+ }
+
+ state.inString = '"';
+ stream.next();
+ return "string";
+ }
+
+ if (stream.match(/[{}\[\]()]/)) {
+ return "punctuation";
+ }
+
+ // Heredoc (<|\.\.\.|==|!=|>=|<=|[=>({
+ startState() {
+ return {
+ inString: null,
+ inBlockScalar: false,
+ blockScalarIndent: 0,
+ };
+ },
+ token(stream, state) {
+ // Block scalar continuation (| or > multiline strings)
+ if (state.inBlockScalar) {
+ // Blank lines are always part of a block scalar.
+ if (stream.match(/^\s*$/)) {
+ stream.skipToEnd();
+ return "string";
+ }
+
+ const indent = stream.indentation();
+
+ if (indent > state.blockScalarIndent) {
+ stream.skipToEnd();
+ return "string";
+ }
+
+ state.inBlockScalar = false;
+ state.blockScalarIndent = 0;
+ }
+
+ // Continue quoted strings across tokens
+ if (state.inString) {
+ let escaped = false;
+
+ while (!stream.eol()) {
+ const next = stream.next();
+
+ if (escaped) {
+ escaped = false;
+ continue;
+ }
+
+ if (next === "\\" && state.inString === '"') {
+ escaped = true;
+ continue;
+ }
+
+ if (next === state.inString) {
+ state.inString = null;
+ break;
+ }
+ }
+
+ return "string";
+ }
+
+ if (stream.eatSpace()) {
+ return null;
+ }
+
+ // Comments
+ if (stream.peek() === "#") {
+ stream.skipToEnd();
+ return "comment";
+ }
+
+ // Document markers
+ if (stream.sol() && (stream.match("---") || stream.match("..."))) {
+ return "keyword";
+ }
+
+ // Anchors & aliases
+ if (stream.match(/[&*][A-Za-z_][\w]*/)) {
+ return "variableName";
+ }
+
+ // CloudFormation intrinsic tags (!Ref, !Sub, !GetAtt, etc.)
+ if (stream.match(/![A-Za-z][A-Za-z0-9]*/)) {
+ return "typeName";
+ }
+
+ // Tags (!!str, !!map, etc.)
+ if (stream.match(/!![A-Za-z]+/)) {
+ return "typeName";
+ }
+
+ // Quoted strings
+ const quote = stream.peek();
+ if (quote === "'" || quote === '"') {
+ state.inString = quote;
+ stream.next();
+ return "string";
+ }
+
+ // Block scalar indicators (| or >)
+ if (
+ stream.sol() === false &&
+ (stream.peek() === "|" || stream.peek() === ">")
+ ) {
+ const prevChar = stream.string.charAt(stream.pos - 1);
+
+ if (prevChar === " " || prevChar === ":") {
+ stream.next();
+ // Eat optional modifiers like |-, |+, |2
+ stream.match(/[-+]?\d?/);
+ state.inBlockScalar = true;
+ state.blockScalarIndent = stream.indentation();
+ return "operator";
+ }
+ }
+
+ // List item marker
+ if (stream.match(/^-(?=\s)/)) {
+ return "punctuation";
+ }
+
+ // Key: value pattern — supports CloudFormation long-form intrinsics (Fn::Sub:)
+ if (stream.match(/[A-Za-z_][\w./-]*(?:::[A-Za-z_][\w]*)*(?=\s*:(?!:))/)) {
+ return "propertyName";
+ }
+
+ // Booleans & null (YAML spec values)
+ if (stream.match(/\b(?:true|false|yes|no|on|off|null)\b/i)) {
+ return "keyword";
+ }
+
+ // Numbers (integers, floats, hex, octal)
+ if (
+ stream.match(
+ /^[-+]?(?:0x[0-9a-fA-F]+|0o[0-7]+|0b[01]+|\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/,
+ )
+ ) {
+ return "number";
+ }
+
+ // Colon separator
+ if (stream.match(":")) {
+ return "punctuation";
+ }
+
+ // Braces/brackets (flow style)
+ if (stream.match(/[{}\[\],]/)) {
+ return "punctuation";
+ }
+
+ // Tilde (null alias)
+ if (stream.match("~")) {
+ return "keyword";
+ }
+
+ // Unquoted strings / values — consume word
+ if (stream.match(/[^\s#:,\[\]{}]+/)) {
+ return "string";
+ }
+
+ stream.next();
+ return null;
+ },
+});
+
+function readBicepStringSegment(
+ stream: StringStream,
+ includeOpeningQuote = false,
+) {
+ if (includeOpeningQuote) {
+ stream.next();
+ }
+
+ while (!stream.eol()) {
+ if (stream.match("${")) {
+ stream.backUp(2);
+ break;
+ }
+
+ const next = stream.next();
+
+ if (next === "'" && stream.peek() === "'") {
+ stream.next();
+ continue;
+ }
+
+ if (next === "'") {
+ stream.backUp(1);
+ break;
+ }
+ }
+}
+
+const bicepLanguage = StreamLanguage.define({
+ startState() {
+ return {
+ inBlockComment: false,
+ inString: null,
+ expectResourceType: false,
+ };
+ },
+ token(stream, state) {
+ if (state.inBlockComment) {
+ while (!stream.eol()) {
+ if (stream.match("*/")) {
+ state.inBlockComment = false;
+ break;
+ }
+
+ stream.next();
+ }
+
+ return "comment";
+ }
+
+ if (state.inString) {
+ if (stream.match("${")) {
+ let depth = 1;
+
+ while (!stream.eol() && depth > 0) {
+ const next = stream.next();
+
+ if (next === "{") {
+ depth += 1;
+ continue;
+ }
+
+ if (next === "}") {
+ depth -= 1;
+ }
+ }
+
+ return "variableName";
+ }
+
+ readBicepStringSegment(stream);
+
+ if (stream.peek() !== "'") {
+ return "string";
+ }
+
+ if (stream.peek() === "'") {
+ state.inString = null;
+ stream.next();
+ }
+
+ return "string";
+ }
+
+ if (stream.eatSpace()) {
+ return null;
+ }
+
+ if (stream.match("//")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+
+ if (stream.match("/*")) {
+ state.inBlockComment = true;
+ return "comment";
+ }
+
+ if (stream.match(/@[A-Za-z_][\w]*/)) {
+ const decorator = stream.current().slice(1);
+
+ if (BICEP_DECORATORS.has(decorator)) {
+ return "keyword";
+ }
+
+ return "keyword";
+ }
+
+ if (stream.peek() === "'") {
+ if (state.expectResourceType) {
+ state.expectResourceType = false;
+ // Consume the full quoted resource type including both quotes
+ stream.next(); // opening '
+ while (!stream.eol()) {
+ const ch = stream.next();
+ if (ch === "'" && stream.peek() === "'") {
+ stream.next(); // escaped ''
+ continue;
+ }
+ if (ch === "'") {
+ break; // closing '
+ }
+ }
+ return "typeName";
+ }
+
+ stream.next(); // consume opening '
+ state.inString = "'";
+ return "string";
+ }
+
+ if (stream.match(/[A-Za-z_][\w-]*(?=\s*:)/)) {
+ return "propertyName";
+ }
+
+ if (stream.match(/\?\?|==|!=|>=|<=|=|>|<|\?|:|!/)) {
+ return "operator";
+ }
+
+ if (stream.match(/[{}\[\]()]/)) {
+ return "punctuation";
+ }
+
+ if (stream.match(/-?\d+(?:\.\d+)?/)) {
+ return "number";
+ }
+
+ if (stream.match(/[A-Za-z_][\w]*/)) {
+ const currentValue = stream.current();
+
+ if (BICEP_KEYWORDS.has(currentValue)) {
+ state.expectResourceType =
+ currentValue === "resource" || currentValue === "module";
+ return "keyword";
+ }
+
+ if (
+ BICEP_FUNCTIONS.has(currentValue) &&
+ stream.match(/\s*(?=\()/, false)
+ ) {
+ return "function";
+ }
+
+ return "variableName";
+ }
+
+ stream.next();
+ return null;
+ },
+});
+
const lightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: "#0550ae", fontWeight: "600" },
{ tag: tags.string, color: "#0a3069" },
@@ -380,6 +1167,14 @@ export const QueryCodeEditor = ({
openCypherLanguage,
syntaxHighlighting(editorHighlightStyle),
);
+ } else if (language === QUERY_EDITOR_LANGUAGE.SHELL) {
+ extensions.push(shellLanguage, syntaxHighlighting(editorHighlightStyle));
+ } else if (language === QUERY_EDITOR_LANGUAGE.HCL) {
+ extensions.push(hclLanguage, syntaxHighlighting(editorHighlightStyle));
+ } else if (language === QUERY_EDITOR_LANGUAGE.BICEP) {
+ extensions.push(bicepLanguage, syntaxHighlighting(editorHighlightStyle));
+ } else if (language === QUERY_EDITOR_LANGUAGE.YAML) {
+ extensions.push(yamlLanguage, syntaxHighlighting(editorHighlightStyle));
}
const handleCopy = async () => {
diff --git a/ui/components/ui/custom/custom-link.test.ts b/ui/components/ui/custom/custom-link.test.ts
new file mode 100644
index 0000000000..1f5a47a054
--- /dev/null
+++ b/ui/components/ui/custom/custom-link.test.ts
@@ -0,0 +1,17 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("custom link", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "custom-link.tsx");
+ const source = readFileSync(filePath, "utf8");
+
+ it("renders external or placeholder hrefs as plain anchors instead of next/link", () => {
+ expect(source).toContain("isExternalHref");
+ expect(source).toContain("hasDynamicHrefPlaceholder");
+ expect(source).toContain(" {
+interface CustomLinkProps extends AnchorHTMLAttributes {
href: string;
target?: "_self" | "_blank" | string;
ariaLabel?: string;
className?: string;
- children: React.ReactNode;
+ children: ReactNode;
scroll?: boolean;
size?: string;
}
-export const CustomLink = React.forwardRef(
+function isExternalHref(href: string) {
+ return /^https?:\/\//.test(href) || href.startsWith("mailto:");
+}
+
+function hasDynamicHrefPlaceholder(href: string) {
+ return href.includes("[") && href.includes("]");
+}
+
+export const CustomLink = forwardRef(
(
{
href,
@@ -28,6 +35,29 @@ export const CustomLink = React.forwardRef(
},
ref,
) => {
+ const linkClassName = cn(
+ `text-${size} text-button-tertiary p-0`,
+ className,
+ );
+ const shouldUseAnchor =
+ isExternalHref(href) || hasDynamicHrefPlaceholder(href);
+
+ if (shouldUseAnchor) {
+ return (
+
+ {children}
+
+ );
+ }
+
return (
(
aria-label={ariaLabel}
target={target}
rel={target === "_blank" ? "noopener noreferrer" : undefined}
- className={cn(`text-${size} text-button-tertiary p-0`, className)}
+ className={linkClassName}
{...props}
>
{children}
diff --git a/ui/components/ui/table/data-table-filter-custom-batch.test.tsx b/ui/components/ui/table/data-table-filter-custom-batch.test.tsx
index f108c9d70b..a8a629d24c 100644
--- a/ui/components/ui/table/data-table-filter-custom-batch.test.tsx
+++ b/ui/components/ui/table/data-table-filter-custom-batch.test.tsx
@@ -62,8 +62,16 @@ vi.mock("@/components/shadcn/select/multiselect", () => ({
MultiSelectValue: ({ placeholder }: { placeholder: string }) => (
{placeholder}
),
- MultiSelectContent: ({ children }: { children: React.ReactNode }) => (
- <>{children}>
+ MultiSelectContent: ({
+ children,
+ width,
+ }: {
+ children: React.ReactNode;
+ width?: string;
+ }) => (
+
+ {children}
+
),
MultiSelectSelectAll: ({ children }: { children: React.ReactNode }) => (
{children}
@@ -114,6 +122,13 @@ const severityFilter: FilterOption = {
values: ["critical", "high"],
};
+const scanFilter: FilterOption = {
+ key: "filter[scan__in]",
+ labelCheckboxGroup: "Scan ID",
+ values: ["scan-1"],
+ width: "wide",
+};
+
describe("DataTableFilterCustom — batch vs instant mode", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -275,4 +290,15 @@ describe("DataTableFilterCustom — batch vs instant mode", () => {
expect(screen.getByRole("button", { name: "Clear" })).toBeInTheDocument();
});
});
+
+ describe("dropdown width", () => {
+ it("should propagate the filter width to the dropdown content", () => {
+ render( );
+
+ expect(screen.getByTestId("multiselect-content")).toHaveAttribute(
+ "data-width",
+ "wide",
+ );
+ });
+ });
});
diff --git a/ui/components/ui/table/data-table-filter-custom.tsx b/ui/components/ui/table/data-table-filter-custom.tsx
index af302592da..28988b34d8 100644
--- a/ui/components/ui/table/data-table-filter-custom.tsx
+++ b/ui/components/ui/table/data-table-filter-custom.tsx
@@ -15,7 +15,12 @@ import {
} from "@/components/shadcn/select/multiselect";
import { EntityInfo } from "@/components/ui/entities/entity-info";
import { useUrlFilters } from "@/hooks/use-url-filters";
-import { isConnectionStatus, isScanEntity } from "@/lib/helper-filters";
+import {
+ getScanEntityLabel,
+ isConnectionStatus,
+ isScanEntity,
+} from "@/lib/helper-filters";
+import { cn } from "@/lib/utils";
import {
FilterEntity,
FilterOption,
@@ -29,6 +34,8 @@ export interface DataTableFilterCustomProps {
filters: FilterOption[];
/** Optional element to render at the start of the filters grid */
prependElement?: React.ReactNode;
+ /** Optional className override for the filters grid layout */
+ gridClassName?: string;
/** Hide the clear filters button and active badges (useful when parent manages this) */
hideClearButton?: boolean;
/**
@@ -54,6 +61,7 @@ export interface DataTableFilterCustomProps {
export const DataTableFilterCustom = ({
filters,
prependElement,
+ gridClassName,
hideClearButton = false,
mode = DATA_TABLE_FILTER_MODE.INSTANT,
onBatchChange,
@@ -80,10 +88,11 @@ export const DataTableFilterCustom = ({
if (!entity) return value;
if (isScanEntity(entity as ScanEntity)) {
- const scanEntity = entity as ScanEntity;
- return (
- scanEntity.providerInfo?.alias || scanEntity.providerInfo?.uid || value
- );
+ // Match the summary-strip chip: "Scan: {provider} - {name}". Without the
+ // "Scan:" prefix, the trigger badge would just say "AWS Prod - Nightly",
+ // which reads as a generic account tag and hides that it's a scan filter.
+ const label = getScanEntityLabel(entity as ScanEntity);
+ return label ? `Scan: ${label}` : value;
}
if (isConnectionStatus(entity)) {
const connectionStatus = entity as ProviderConnectionStatus;
@@ -173,7 +182,12 @@ export const DataTableFilterCustom = ({
};
return (
-
+
{prependElement}
{sortedFilters().map((filter) => {
const selectedValues = getSelectedValues(filter);
@@ -189,7 +203,10 @@ export const DataTableFilterCustom = ({
placeholder={`All ${filter.labelCheckboxGroup}`}
/>
-
+
Select All
{filter.values.map((value) => {
diff --git a/ui/components/ui/table/data-table.tsx b/ui/components/ui/table/data-table.tsx
index 339a5ab6d0..7cf78271a0 100644
--- a/ui/components/ui/table/data-table.tsx
+++ b/ui/components/ui/table/data-table.tsx
@@ -110,6 +110,8 @@ interface DataTableProviderProps {
searchBadge?: { label: string; onDismiss: () => void };
/** Optional click handler for top-level rows. */
onRowClick?: (row: Row) => void;
+ /** Optional header rendered inside the table container, above the toolbar. */
+ header?: ReactNode;
}
export function DataTable({
@@ -140,6 +142,7 @@ export function DataTable({
renderAfterRow,
searchBadge,
onRowClick,
+ header,
}: DataTableProviderProps) {
const [sorting, setSorting] = useState([]);
const [columnFilters, setColumnFilters] = useState([]);
@@ -235,6 +238,7 @@ export function DataTable({
isPending && "pointer-events-none opacity-60",
)}
>
+ {header && {header}
}
{/* Table Toolbar */}
{showToolbar && (
diff --git a/ui/dependency-log.json b/ui/dependency-log.json
index 5302635439..e8e847fa22 100644
--- a/ui/dependency-log.json
+++ b/ui/dependency-log.json
@@ -1015,14 +1015,6 @@
"strategy": "installed",
"generatedAt": "2026-01-19T13:54:24.770Z"
},
- {
- "section": "devDependencies",
- "name": "husky",
- "from": "9.1.7",
- "to": "9.1.7",
- "strategy": "installed",
- "generatedAt": "2025-10-22T12:36:37.962Z"
- },
{
"section": "devDependencies",
"name": "jsdom",
@@ -1031,14 +1023,6 @@
"strategy": "installed",
"generatedAt": "2026-01-29T16:42:27.795Z"
},
- {
- "section": "devDependencies",
- "name": "lint-staged",
- "from": "15.5.2",
- "to": "15.5.2",
- "strategy": "installed",
- "generatedAt": "2025-10-22T12:36:37.962Z"
- },
{
"section": "devDependencies",
"name": "postcss",
diff --git a/ui/docs/code-review/CODE_REVIEW_QUICK_START.md b/ui/docs/code-review/CODE_REVIEW_QUICK_START.md
deleted file mode 100644
index 91dd2ad4af..0000000000
--- a/ui/docs/code-review/CODE_REVIEW_QUICK_START.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# Code Review - Quick Start
-
-## 3 Steps to Enable
-
-### 1. Open `.env`
-```bash
-nano ui/.env
-# or your favorite editor
-```
-
-### 2. Find this line
-```bash
-CODE_REVIEW_ENABLED=false
-```
-
-### 3. Change it to
-```bash
-CODE_REVIEW_ENABLED=true
-```
-
-**Done! ✅**
-
----
-
-## What Happens Now
-
-Every time you `git commit`:
-
-```
-✅ If your code complies with AGENTS.md standards:
- → Commit executes normally
-
-❌ If there are standard violations:
- → Commit is BLOCKED
- → You see the errors in the terminal
- → Fix the code
- → Commit again
-```
-
----
-
-## Example
-
-```bash
-$ git commit -m "feat: add new component"
-
-🏁 Prowler UI - Pre-Commit Hook
-
-ℹ️ Code Review Status: true
-
-🔍 Running Claude Code standards validation...
-
-📋 Files to validate:
- - components/my-feature.tsx
-
-📤 Sending to Claude Code for validation...
-
-STATUS: FAILED
-- File: components/my-feature.tsx:45
- Rule: React Imports
- Issue: Using 'import * as React'
- Expected: import { useState } from "react"
-
-❌ VALIDATION FAILED
-Fix violations before committing
-
-# Fix the file and commit again
-$ git commit -m "feat: add new component"
-
-🏁 Prowler UI - Pre-Commit Hook
-
-ℹ️ Code Review Status: true
-
-🔍 Running Claude Code standards validation...
-
-✅ VALIDATION PASSED
-
-# Commit successful ✅
-```
-
----
-
-## Disable Temporarily
-
-If you need to commit without validation:
-
-```bash
-# Option 1: Change in .env
-CODE_REVIEW_ENABLED=false
-
-# Option 2: Bypass (use with caution!)
-git commit --no-verify
-```
-
----
-
-## What Gets Validated
-
-- ✅ Correct React imports
-- ✅ TypeScript patterns (const-based types)
-- ✅ Tailwind CSS (no var() or hex in className)
-- ✅ cn() utility (only for conditionals)
-- ✅ No useMemo/useCallback without reason
-- ✅ Zod v4 syntax
-- ✅ File organization
-- ✅ Directives "use client"/"use server"
-
----
-
-## More Info
-
-Read `CODE_REVIEW_SETUP.md` for:
-- Troubleshooting
-- Complete details
-- Advanced configuration
diff --git a/ui/hooks/use-filter-batch.test.ts b/ui/hooks/use-filter-batch.test.ts
index 675316402e..8107b7d357 100644
--- a/ui/hooks/use-filter-batch.test.ts
+++ b/ui/hooks/use-filter-batch.test.ts
@@ -61,6 +61,43 @@ describe("useFilterBatch", () => {
});
expect(result.current.hasChanges).toBe(false);
});
+
+ it("should expose filter[delta]=new under the FilterType.DELTA key so the dropdown shows it selected", async () => {
+ // Given — URL from LinkToFindings uses `filter[delta]` (singular), matching the API.
+ setSearchParams({
+ "filter[status__in]": "FAIL",
+ "filter[delta]": "new",
+ });
+
+ const { FilterType } = await import("@/types/filters");
+
+ // When
+ const { result } = renderHook(() => useFilterBatch());
+
+ // Then — the Delta dropdown reads via getFilterValue(`filter[${FilterType.DELTA}]`).
+ // For the checkbox of "new" to appear checked, that lookup must return ["new"].
+ expect(
+ result.current.getFilterValue(`filter[${FilterType.DELTA}]`),
+ ).toEqual(["new"]);
+ });
+
+ it("should include both filter[status__in] and filter[delta] from the overview deep link", () => {
+ // Given — URL produced by LinkToFindings: /findings?...&filter[status__in]=FAIL&filter[delta]=new
+ setSearchParams({
+ "filter[status__in]": "FAIL",
+ "filter[delta]": "new",
+ });
+
+ // When
+ const { result } = renderHook(() => useFilterBatch());
+
+ // Then — the singular `filter[delta]` key must be captured in pendingFilters
+ // so FindingsFilters can render a chip for it (same as filter[status__in]).
+ expect(result.current.pendingFilters).toEqual({
+ "filter[status__in]": ["FAIL"],
+ "filter[delta]": ["new"],
+ });
+ });
});
// ── Excluded keys ──────────────────────────────────────────────────────────
diff --git a/ui/hooks/use-finding-group-resource-state.test.ts b/ui/hooks/use-finding-group-resource-state.test.ts
new file mode 100644
index 0000000000..789ea592c2
--- /dev/null
+++ b/ui/hooks/use-finding-group-resource-state.test.ts
@@ -0,0 +1,15 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("useFindingGroupResourceState", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const filePath = path.join(currentDir, "use-finding-group-resource-state.ts");
+ const source = readFileSync(filePath, "utf8");
+
+ it("enables muted findings only for the finding-group resource drawer", () => {
+ expect(source).toContain("includeMutedInOtherFindings: true");
+ });
+});
diff --git a/ui/hooks/use-finding-group-resource-state.ts b/ui/hooks/use-finding-group-resource-state.ts
new file mode 100644
index 0000000000..309a69a6dc
--- /dev/null
+++ b/ui/hooks/use-finding-group-resource-state.ts
@@ -0,0 +1,160 @@
+"use client";
+
+import { OnChangeFn, Row, RowSelectionState } from "@tanstack/react-table";
+import { useState } from "react";
+
+import { canMuteFindingResource } from "@/components/findings/table/finding-resource-selection";
+import { useResourceDetailDrawer } from "@/components/findings/table/resource-detail-drawer";
+import { useFindingGroupResources } from "@/hooks/use-finding-group-resources";
+import { FindingGroupRow, FindingResourceRow } from "@/types";
+
+interface UseFindingGroupResourceStateOptions {
+ group: FindingGroupRow;
+ filters: Record
;
+ hasHistoricalData: boolean;
+ onResourceSelectionChange?: (findingIds: string[]) => void;
+ scrollContainerRef?: React.RefObject;
+}
+
+interface UseFindingGroupResourceStateReturn {
+ rowSelection: RowSelectionState;
+ resources: FindingResourceRow[];
+ isLoading: boolean;
+ sentinelRef: (node: HTMLDivElement | null) => void;
+ refresh: () => void;
+ loadMore: () => void;
+ totalCount: number | null;
+ drawer: ReturnType;
+ handleDrawerMuteComplete: () => void;
+ selectedFindingIds: string[];
+ selectableRowCount: number;
+ getRowCanSelect: (row: Row) => boolean;
+ clearSelection: () => void;
+ isSelected: (id: string) => boolean;
+ handleMuteComplete: () => void;
+ handleRowSelectionChange: OnChangeFn;
+ resolveSelectedFindingIds: (ids: string[]) => Promise;
+}
+
+export function useFindingGroupResourceState({
+ group,
+ filters,
+ hasHistoricalData,
+ onResourceSelectionChange,
+ scrollContainerRef,
+}: UseFindingGroupResourceStateOptions): UseFindingGroupResourceStateReturn {
+ const [rowSelection, setRowSelection] = useState({});
+ const [resources, setResources] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const handleSetResources = (
+ newResources: FindingResourceRow[],
+ _hasMore: boolean,
+ ) => {
+ setResources(newResources);
+ setIsLoading(false);
+ };
+
+ const handleAppendResources = (
+ newResources: FindingResourceRow[],
+ _hasMore: boolean,
+ ) => {
+ setResources((prev) => [...prev, ...newResources]);
+ setIsLoading(false);
+ };
+
+ const handleSetLoading = (loading: boolean) => {
+ setIsLoading(loading);
+ };
+
+ const { sentinelRef, refresh, loadMore, totalCount } =
+ useFindingGroupResources({
+ checkId: group.checkId,
+ hasDateOrScanFilter: hasHistoricalData,
+ filters,
+ onSetResources: handleSetResources,
+ onAppendResources: handleAppendResources,
+ onSetLoading: handleSetLoading,
+ scrollContainerRef,
+ });
+
+ const drawer = useResourceDetailDrawer({
+ resources,
+ totalResourceCount: totalCount ?? group.resourcesTotal,
+ onRequestMoreResources: loadMore,
+ canLoadOtherFindings: group.resourcesTotal !== 0,
+ includeMutedInOtherFindings: true,
+ });
+
+ const handleDrawerMuteComplete = () => {
+ drawer.refetchCurrent();
+ refresh();
+ };
+
+ const selectedFindingIds = Object.keys(rowSelection)
+ .filter((key) => rowSelection[key])
+ .map((idx) => resources[parseInt(idx)]?.findingId)
+ .filter((id): id is string => Boolean(id));
+
+ const selectableRowCount = resources.filter(canMuteFindingResource).length;
+
+ const getRowCanSelect = (row: Row): boolean => {
+ return canMuteFindingResource(row.original);
+ };
+
+ const clearSelection = () => {
+ setRowSelection({});
+ onResourceSelectionChange?.([]);
+ };
+
+ const isSelected = (id: string) => {
+ return selectedFindingIds.includes(id);
+ };
+
+ const handleMuteComplete = () => {
+ clearSelection();
+ refresh();
+ };
+
+ const handleRowSelectionChange = (
+ updater:
+ | RowSelectionState
+ | ((prev: RowSelectionState) => RowSelectionState),
+ ) => {
+ const newSelection =
+ typeof updater === "function" ? updater(rowSelection) : updater;
+ setRowSelection(newSelection);
+
+ if (onResourceSelectionChange) {
+ const newFindingIds = Object.keys(newSelection)
+ .filter((key) => newSelection[key])
+ .map((idx) => resources[parseInt(idx)]?.findingId)
+ .filter((id): id is string => Boolean(id));
+ onResourceSelectionChange(newFindingIds);
+ }
+ };
+
+ const resolveSelectedFindingIds = async (ids: string[]) => {
+ return ids.filter(Boolean);
+ };
+
+ return {
+ rowSelection,
+ resources,
+ isLoading,
+ sentinelRef,
+ refresh,
+ loadMore,
+ totalCount,
+ drawer,
+ handleDrawerMuteComplete,
+ selectedFindingIds,
+ selectableRowCount,
+ getRowCanSelect,
+ clearSelection,
+ isSelected,
+ handleMuteComplete,
+ handleRowSelectionChange,
+ resolveSelectedFindingIds,
+ };
+}
diff --git a/ui/hooks/use-finding-group-resources.test.ts b/ui/hooks/use-finding-group-resources.test.ts
new file mode 100644
index 0000000000..c2fe2e4145
--- /dev/null
+++ b/ui/hooks/use-finding-group-resources.test.ts
@@ -0,0 +1,206 @@
+import { act, renderHook } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { useFindingGroupResources } from "./use-finding-group-resources";
+
+type IntersectionCallback = (entries: IntersectionObserverEntry[]) => void;
+
+let latestObserverCallback: IntersectionCallback | null = null;
+
+class MockIntersectionObserver {
+ callback: IntersectionCallback;
+ constructor(callback: IntersectionCallback) {
+ this.callback = callback;
+ latestObserverCallback = callback;
+ }
+ observe() {}
+ unobserve() {}
+ disconnect() {
+ if (latestObserverCallback === this.callback) {
+ latestObserverCallback = null;
+ }
+ }
+}
+
+function triggerIntersection() {
+ latestObserverCallback?.([
+ { isIntersecting: true } as IntersectionObserverEntry,
+ ]);
+}
+
+const findingGroupActionsMock = vi.hoisted(() => ({
+ getLatestFindingGroupResources: vi.fn(),
+ getFindingGroupResources: vi.fn(),
+ adaptFindingGroupResourcesResponse: vi.fn(),
+}));
+
+vi.mock("@/actions/finding-groups", () => findingGroupActionsMock);
+
+function makeApiResponse(
+ resources: { id: string }[],
+ { pages = 1 }: { pages?: number } = {},
+) {
+ return {
+ data: resources,
+ meta: { pagination: { pages } },
+ };
+}
+
+function fakeResource(id: string) {
+ return {
+ findingId: id,
+ resourceUid: `uid-${id}`,
+ resourceName: `Resource ${id}`,
+ status: "FAIL",
+ severity: "high",
+ isMuted: false,
+ };
+}
+
+function defaultOptions(overrides?: Record) {
+ return {
+ checkId: "check_1",
+ hasDateOrScanFilter: false,
+ filters: {},
+ onSetResources: vi.fn(),
+ onAppendResources: vi.fn(),
+ onSetLoading: vi.fn(),
+ ...overrides,
+ };
+}
+
+async function flushAsync() {
+ await act(async () => {
+ await new Promise((r) => setTimeout(r, 0));
+ });
+}
+
+describe("useFindingGroupResources", () => {
+ beforeEach(() => {
+ vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
+ for (const mockFn of Object.values(findingGroupActionsMock)) {
+ mockFn.mockReset();
+ }
+ });
+
+ describe("when mounting", () => {
+ it("should fetch page 1 and deliver resources via onSetResources", async () => {
+ const apiResponse = makeApiResponse([{ id: "r1" }, { id: "r2" }], {
+ pages: 1,
+ });
+ const adapted = [fakeResource("r1"), fakeResource("r2")];
+
+ findingGroupActionsMock.getLatestFindingGroupResources.mockResolvedValue(
+ apiResponse,
+ );
+ findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
+ adapted,
+ );
+
+ const onSetResources = vi.fn();
+ const onSetLoading = vi.fn();
+
+ renderHook(() =>
+ useFindingGroupResources(
+ defaultOptions({ onSetResources, onSetLoading }),
+ ),
+ );
+ await flushAsync();
+
+ expect(
+ findingGroupActionsMock.getLatestFindingGroupResources,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ checkId: "check_1",
+ page: 1,
+ pageSize: 10,
+ }),
+ );
+ expect(onSetResources).toHaveBeenCalledWith(adapted, false);
+ });
+
+ it("should use getFindingGroupResources when hasDateOrScanFilter is true", async () => {
+ const apiResponse = makeApiResponse([], { pages: 1 });
+ findingGroupActionsMock.getFindingGroupResources.mockResolvedValue(
+ apiResponse,
+ );
+ findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
+ [],
+ );
+
+ renderHook(() =>
+ useFindingGroupResources(defaultOptions({ hasDateOrScanFilter: true })),
+ );
+ await flushAsync();
+
+ expect(
+ findingGroupActionsMock.getFindingGroupResources,
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ findingGroupActionsMock.getLatestFindingGroupResources,
+ ).not.toHaveBeenCalled();
+ });
+
+ it("should forward the active finding-group filters to the resources endpoint", async () => {
+ const apiResponse = makeApiResponse([], { pages: 1 });
+ const filters = {
+ "filter[status__in]": "PASS",
+ "filter[severity__in]": "medium",
+ "filter[provider_type__in]": "aws",
+ };
+ findingGroupActionsMock.getLatestFindingGroupResources.mockResolvedValue(
+ apiResponse,
+ );
+ findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
+ [],
+ );
+
+ renderHook(() => useFindingGroupResources(defaultOptions({ filters })));
+ await flushAsync();
+
+ expect(
+ findingGroupActionsMock.getLatestFindingGroupResources,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ checkId: "check_1",
+ page: 1,
+ pageSize: 10,
+ filters,
+ }),
+ );
+ });
+ });
+
+ describe("when all resources fit in one page", () => {
+ it("should not fetch page 2 after page 1 completes", async () => {
+ const apiResponse = makeApiResponse(
+ [{ id: "r1" }, { id: "r2" }, { id: "r3" }, { id: "r4" }],
+ { pages: 1 },
+ );
+ findingGroupActionsMock.getLatestFindingGroupResources.mockResolvedValue(
+ apiResponse,
+ );
+ findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
+ [
+ fakeResource("r1"),
+ fakeResource("r2"),
+ fakeResource("r3"),
+ fakeResource("r4"),
+ ],
+ );
+
+ const { result } = renderHook(() =>
+ useFindingGroupResources(defaultOptions()),
+ );
+ await flushAsync();
+
+ act(() => triggerIntersection());
+ await flushAsync();
+
+ expect(
+ findingGroupActionsMock.getLatestFindingGroupResources,
+ ).toHaveBeenCalledTimes(1);
+ expect(result.current.totalCount).toBeNull();
+ });
+ });
+});
diff --git a/ui/hooks/use-infinite-resources.ts b/ui/hooks/use-finding-group-resources.ts
similarity index 64%
rename from ui/hooks/use-infinite-resources.ts
rename to ui/hooks/use-finding-group-resources.ts
index df710ebf10..7ef4813be0 100644
--- a/ui/hooks/use-infinite-resources.ts
+++ b/ui/hooks/use-finding-group-resources.ts
@@ -12,7 +12,7 @@ import { FindingResourceRow } from "@/types";
const RESOURCES_PAGE_SIZE = 10;
-interface UseInfiniteResourcesOptions {
+interface UseFindingGroupResourcesOptions {
checkId: string;
hasDateOrScanFilter: boolean;
filters: Record;
@@ -22,28 +22,17 @@ interface UseInfiniteResourcesOptions {
hasMore: boolean,
) => void;
onSetLoading: (loading: boolean) => void;
- /** Scroll container element for IntersectionObserver root. Defaults to viewport. */
scrollContainerRef?: React.RefObject;
}
-interface UseInfiniteResourcesReturn {
+interface UseFindingGroupResourcesReturn {
sentinelRef: (node: HTMLDivElement | null) => void;
- /** Reset pagination and re-fetch page 1 (e.g. after muting). */
refresh: () => void;
- /** Imperatively load the next page (e.g. from drawer navigation). */
loadMore: () => void;
- /** Total number of resources matching current filters (from API pagination). */
totalCount: number | null;
}
-/**
- * Hook for paginated infinite-scroll loading of finding group resources.
- *
- * Uses refs for all mutable state to avoid dependency chains that
- * cause infinite re-render loops. The parent component remounts this
- * hook via key-prop when checkId or filters change.
- */
-export function useInfiniteResources({
+export function useFindingGroupResources({
checkId,
hasDateOrScanFilter,
filters,
@@ -51,28 +40,21 @@ export function useInfiniteResources({
onAppendResources,
onSetLoading,
scrollContainerRef,
-}: UseInfiniteResourcesOptions): UseInfiniteResourcesReturn {
- // All mutable state in refs to break dependency chains
+}: UseFindingGroupResourcesOptions): UseFindingGroupResourcesReturn {
const pageRef = useRef(1);
const hasMoreRef = useRef(true);
- // Start as `true` to block the IntersectionObserver from calling loadNextPage
- // before the initial fetch runs. Ref callbacks fire during commit (sync),
- // but useMountEffect fires after paint — the observer can sneak in between.
const isLoadingRef = useRef(true);
const currentCheckIdRef = useRef(checkId);
const controllerRef = useRef(null);
const observerRef = useRef(null);
const totalCountRef = useRef(null);
- // Store latest values in refs so the fetch function always reads current values
- // without being recreated on every render
const hasDateOrScanRef = useRef(hasDateOrScanFilter);
const filtersRef = useRef(filters);
const onSetResourcesRef = useRef(onSetResources);
const onAppendResourcesRef = useRef(onAppendResources);
const onSetLoadingRef = useRef(onSetLoading);
- // Keep refs in sync with latest props
currentCheckIdRef.current = checkId;
hasDateOrScanRef.current = hasDateOrScanFilter;
filtersRef.current = filters;
@@ -103,7 +85,6 @@ export function useInfiniteResources({
filters: filtersRef.current,
});
- // Discard stale response if aborted (e.g. Strict Mode remount)
if (signal.aborted) {
return;
}
@@ -116,10 +97,6 @@ export function useInfiniteResources({
const hasMore = page < totalPages;
totalCountRef.current = response?.meta?.pagination?.count ?? null;
- // Commit the page number only after a successful (non-aborted) fetch.
- // This prevents a premature pageRef increment from loadNextPage being
- // permanently committed if a concurrent abort fires before fetchPage
- // starts executing.
pageRef.current = page;
hasMoreRef.current = hasMore;
@@ -130,27 +107,20 @@ export function useInfiniteResources({
}
} catch (error) {
if (!signal.aborted) {
- console.error("Error fetching resources:", error);
+ console.error("Error fetching finding group resources:", error);
onSetLoadingRef.current(false);
}
} finally {
- // Only release the loading guard if this fetch wasn't aborted.
- // An aborted fetch (e.g. Strict Mode cleanup) must NOT reset the flag
- // while a subsequent fetch from the remount is still in flight.
if (!signal.aborted) {
isLoadingRef.current = false;
}
}
}
- // Fetch first page on mount — parent remounts via key-prop on checkId/filter changes
useMountEffect(() => {
const controller = new AbortController();
controllerRef.current = controller;
- // Release the loading guard so fetchPage can proceed.
- // This is synchronous with the fetchPage call below, so the observer
- // cannot sneak in between these two lines.
isLoadingRef.current = false;
fetchPage(1, false, checkId, controller.signal);
@@ -167,17 +137,13 @@ export function useInfiniteResources({
isLoadingRef.current ||
!signal ||
signal.aborted
- )
+ ) {
return;
+ }
- // Pass the next page number as an argument without pre-committing
- // pageRef.current. The fetchPage function commits pageRef.current = page
- // only after a successful (non-aborted) response, eliminating the race
- // where a concurrent abort would leave pageRef permanently incremented.
fetchPage(pageRef.current + 1, true, currentCheckIdRef.current, signal);
}
- // IntersectionObserver callback
function handleIntersection(entries: IntersectionObserverEntry[]) {
const [entry] = entries;
if (entry.isIntersecting) {
@@ -185,7 +151,6 @@ export function useInfiniteResources({
}
}
- // Set up observer when sentinel node changes
function sentinelRef(node: HTMLDivElement | null) {
if (observerRef.current) {
observerRef.current.disconnect();
@@ -201,7 +166,6 @@ export function useInfiniteResources({
}
}
- /** Imperatively reset and re-fetch page 1 without changing deps. */
function refresh() {
controllerRef.current?.abort();
const controller = new AbortController();
diff --git a/ui/hooks/use-infinite-resources.test.ts b/ui/hooks/use-infinite-resources.test.ts
deleted file mode 100644
index 618de56fba..0000000000
--- a/ui/hooks/use-infinite-resources.test.ts
+++ /dev/null
@@ -1,570 +0,0 @@
-import { act, renderHook } from "@testing-library/react";
-import { beforeEach, describe, expect, it, vi } from "vitest";
-
-import { useInfiniteResources } from "./use-infinite-resources";
-
-// ---------------------------------------------------------------------------
-// IntersectionObserver mock (jsdom doesn't provide one)
-// ---------------------------------------------------------------------------
-
-type IntersectionCallback = (entries: IntersectionObserverEntry[]) => void;
-
-/** Stores the latest observer callback so tests can trigger intersections. */
-let latestObserverCallback: IntersectionCallback | null = null;
-
-class MockIntersectionObserver {
- callback: IntersectionCallback;
- constructor(callback: IntersectionCallback) {
- this.callback = callback;
- latestObserverCallback = callback;
- }
- observe() {}
- unobserve() {}
- disconnect() {
- if (latestObserverCallback === this.callback) {
- latestObserverCallback = null;
- }
- }
-}
-
-/** Simulate the sentinel becoming visible in the scroll container. */
-function triggerIntersection() {
- latestObserverCallback?.([
- { isIntersecting: true } as IntersectionObserverEntry,
- ]);
-}
-
-// ---------------------------------------------------------------------------
-// Mocks
-// ---------------------------------------------------------------------------
-
-const findingGroupActionsMock = vi.hoisted(() => ({
- getLatestFindingGroupResources: vi.fn(),
- getFindingGroupResources: vi.fn(),
- adaptFindingGroupResourcesResponse: vi.fn(),
-}));
-
-vi.mock("@/actions/finding-groups", () => findingGroupActionsMock);
-
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-
-function makeApiResponse(
- resources: { id: string }[],
- { pages = 1 }: { pages?: number } = {},
-) {
- return {
- data: resources,
- meta: { pagination: { pages } },
- };
-}
-
-function fakeResource(id: string) {
- return {
- findingId: id,
- resourceUid: `uid-${id}`,
- resourceName: `Resource ${id}`,
- status: "FAIL",
- severity: "high",
- isMuted: false,
- };
-}
-
-function defaultOptions(overrides?: Record) {
- return {
- checkId: "check_1",
- hasDateOrScanFilter: false,
- filters: {},
- onSetResources: vi.fn(),
- onAppendResources: vi.fn(),
- onSetLoading: vi.fn(),
- ...overrides,
- };
-}
-
-/** Flush all pending microtasks (awaits in fetchPage). */
-async function flushAsync() {
- await act(async () => {
- await new Promise((r) => setTimeout(r, 0));
- });
-}
-
-// ---------------------------------------------------------------------------
-// Tests
-// ---------------------------------------------------------------------------
-
-describe("useInfiniteResources", () => {
- beforeEach(() => {
- vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
- for (const mockFn of Object.values(findingGroupActionsMock)) {
- mockFn.mockReset();
- }
- });
-
- describe("when mounting", () => {
- it("should fetch page 1 and deliver resources via onSetResources", async () => {
- // Given
- const apiResponse = makeApiResponse([{ id: "r1" }, { id: "r2" }], {
- pages: 1,
- });
- const adapted = [fakeResource("r1"), fakeResource("r2")];
-
- findingGroupActionsMock.getLatestFindingGroupResources.mockResolvedValue(
- apiResponse,
- );
- findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
- adapted,
- );
-
- const onSetResources = vi.fn();
- const onSetLoading = vi.fn();
-
- // When
- renderHook(() =>
- useInfiniteResources(defaultOptions({ onSetResources, onSetLoading })),
- );
- await flushAsync();
-
- // Then
- expect(
- findingGroupActionsMock.getLatestFindingGroupResources,
- ).toHaveBeenCalledWith(
- expect.objectContaining({
- checkId: "check_1",
- page: 1,
- pageSize: 10,
- }),
- );
- expect(onSetResources).toHaveBeenCalledWith(adapted, false);
- });
-
- it("should use getFindingGroupResources when hasDateOrScanFilter is true", async () => {
- // Given
- const apiResponse = makeApiResponse([], { pages: 1 });
- findingGroupActionsMock.getFindingGroupResources.mockResolvedValue(
- apiResponse,
- );
- findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
- [],
- );
-
- // When
- renderHook(() =>
- useInfiniteResources(defaultOptions({ hasDateOrScanFilter: true })),
- );
- await flushAsync();
-
- // Then
- expect(
- findingGroupActionsMock.getFindingGroupResources,
- ).toHaveBeenCalledTimes(1);
- expect(
- findingGroupActionsMock.getLatestFindingGroupResources,
- ).not.toHaveBeenCalled();
- });
-
- it("should forward the active finding-group filters to the resources endpoint", async () => {
- // Given
- const apiResponse = makeApiResponse([], { pages: 1 });
- const filters = {
- "filter[status__in]": "PASS",
- "filter[severity__in]": "medium",
- "filter[provider_type__in]": "aws",
- };
- findingGroupActionsMock.getLatestFindingGroupResources.mockResolvedValue(
- apiResponse,
- );
- findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
- [],
- );
-
- // When
- renderHook(() => useInfiniteResources(defaultOptions({ filters })));
- await flushAsync();
-
- // Then
- expect(
- findingGroupActionsMock.getLatestFindingGroupResources,
- ).toHaveBeenCalledWith(
- expect.objectContaining({
- checkId: "check_1",
- page: 1,
- pageSize: 10,
- filters,
- }),
- );
- });
- });
-
- describe("when all resources fit in one page", () => {
- it("should not fetch page 2 after page 1 completes", async () => {
- // Given — API returns 4 resources, 1 page total
- const apiResponse = makeApiResponse(
- [{ id: "r1" }, { id: "r2" }, { id: "r3" }, { id: "r4" }],
- { pages: 1 },
- );
- findingGroupActionsMock.getLatestFindingGroupResources.mockResolvedValue(
- apiResponse,
- );
- findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
- [
- fakeResource("r1"),
- fakeResource("r2"),
- fakeResource("r3"),
- fakeResource("r4"),
- ],
- );
-
- // When
- const { result } = renderHook(() =>
- useInfiniteResources(defaultOptions()),
- );
- await flushAsync();
-
- // Attach sentinel so observer is created
- const sentinel = document.createElement("div");
- act(() => {
- result.current.sentinelRef(sentinel);
- });
-
- // Simulate observer firing (sentinel visible after page 1 loaded)
- act(() => {
- triggerIntersection();
- });
- await flushAsync();
-
- // Then — only page 1 was fetched, never page 2
- const calls =
- findingGroupActionsMock.getLatestFindingGroupResources.mock.calls;
- const pageNumbers = calls.map(
- (c: unknown[]) => (c[0] as { page: number }).page,
- );
- expect(pageNumbers.every((p: number) => p === 1)).toBe(true);
- });
- });
-
- describe("when aborted fetch races with active fetch", () => {
- it("should not reset isLoading when an aborted fetch resolves", async () => {
- // Given — simulate the Strict Mode race condition:
- // fetch1 starts, gets aborted, fetch2 starts, fetch1's finally runs
- const onSetResources = vi.fn();
- const onSetLoading = vi.fn();
-
- // fetch1 resolves slowly (after abort)
- let resolveFetch1: (v: unknown) => void;
- const fetch1Promise = new Promise((r) => {
- resolveFetch1 = r;
- });
-
- // fetch2 resolves normally
- const apiResponse = makeApiResponse([{ id: "r1" }], { pages: 1 });
- const adapted = [fakeResource("r1")];
-
- let callCount = 0;
- findingGroupActionsMock.getLatestFindingGroupResources.mockImplementation(
- () => {
- callCount++;
- if (callCount === 1) return fetch1Promise;
- return Promise.resolve(apiResponse);
- },
- );
- findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
- adapted,
- );
-
- // When — mount, abort (simulating cleanup), remount
- const { unmount } = renderHook(() =>
- useInfiniteResources(defaultOptions({ onSetResources, onSetLoading })),
- );
-
- // Simulate Strict Mode: unmount triggers abort
- unmount();
-
- // Fetch1 resolves AFTER abort — its finally should NOT reset isLoading
- await act(async () => {
- resolveFetch1!(apiResponse);
- await new Promise((r) => setTimeout(r, 0));
- });
-
- // Then — onSetResources should NOT have been called by the aborted fetch
- // (the signal.aborted check returns early)
- expect(onSetResources).not.toHaveBeenCalled();
- });
- });
-
- describe("when sentinel triggers next page", () => {
- it("should fetch page 2 via onAppendResources when hasMore is true", async () => {
- // Given — page 1 has more pages
- const page1Response = makeApiResponse(
- Array.from({ length: 10 }, (_, i) => ({ id: `r${i}` })),
- { pages: 3 },
- );
- const page1Adapted = Array.from({ length: 10 }, (_, i) =>
- fakeResource(`r${i}`),
- );
-
- const page2Response = makeApiResponse(
- Array.from({ length: 10 }, (_, i) => ({ id: `r${10 + i}` })),
- { pages: 3 },
- );
- const page2Adapted = Array.from({ length: 10 }, (_, i) =>
- fakeResource(`r${10 + i}`),
- );
-
- findingGroupActionsMock.getLatestFindingGroupResources
- .mockResolvedValueOnce(page1Response)
- .mockResolvedValueOnce(page2Response);
- findingGroupActionsMock.adaptFindingGroupResourcesResponse
- .mockReturnValueOnce(page1Adapted)
- .mockReturnValueOnce(page2Adapted);
-
- const onSetResources = vi.fn();
- const onAppendResources = vi.fn();
-
- // When — mount and wait for page 1
- const { result } = renderHook(() =>
- useInfiniteResources(
- defaultOptions({ onSetResources, onAppendResources }),
- ),
- );
- await flushAsync();
-
- expect(onSetResources).toHaveBeenCalledWith(page1Adapted, true);
-
- // Attach sentinel and simulate intersection → triggers page 2
- const sentinel = document.createElement("div");
- act(() => {
- result.current.sentinelRef(sentinel);
- });
- act(() => {
- triggerIntersection();
- });
- await flushAsync();
-
- // Then
- expect(onAppendResources).toHaveBeenCalledWith(page2Adapted, true);
- expect(
- findingGroupActionsMock.getLatestFindingGroupResources,
- ).toHaveBeenCalledTimes(2);
- });
- });
-
- describe("when refresh is called", () => {
- it("should re-fetch page 1 and deliver via onSetResources", async () => {
- // Given
- const apiResponse = makeApiResponse([{ id: "r1" }], { pages: 1 });
- const adapted = [fakeResource("r1")];
-
- findingGroupActionsMock.getLatestFindingGroupResources.mockResolvedValue(
- apiResponse,
- );
- findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
- adapted,
- );
-
- const onSetResources = vi.fn();
-
- const { result } = renderHook(() =>
- useInfiniteResources(defaultOptions({ onSetResources })),
- );
- await flushAsync();
-
- expect(onSetResources).toHaveBeenCalledTimes(1);
-
- // When — refresh (e.g. after muting)
- act(() => {
- result.current.refresh();
- });
- await flushAsync();
-
- // Then — page 1 fetched again
- expect(onSetResources).toHaveBeenCalledTimes(2);
- const calls =
- findingGroupActionsMock.getLatestFindingGroupResources.mock.calls;
- expect(calls).toHaveLength(2);
- expect(calls[0][0].page).toBe(1);
- expect(calls[1][0].page).toBe(1);
- });
- });
-
- describe("when filters include search params", () => {
- it("should pass filters to the fetch function", async () => {
- // Given
- const apiResponse = makeApiResponse([], { pages: 1 });
- findingGroupActionsMock.getLatestFindingGroupResources.mockResolvedValue(
- apiResponse,
- );
- findingGroupActionsMock.adaptFindingGroupResourcesResponse.mockReturnValue(
- [],
- );
-
- const filters = {
- "filter[name__icontains]": "my-resource",
- "filter[severity__in]": "high",
- };
-
- // When
- renderHook(() => useInfiniteResources(defaultOptions({ filters })));
- await flushAsync();
-
- // Then
- expect(
- findingGroupActionsMock.getLatestFindingGroupResources,
- ).toHaveBeenCalledWith(expect.objectContaining({ filters }));
- });
- });
-
- describe("when refresh() fires while loadNextPage is in-flight (race condition — Fix 5)", () => {
- it("should discard in-flight page 2 and fetch page 1 when refresh fires during loadNextPage", async () => {
- // Given — page 1 has 2 pages total, page 2 hangs indefinitely
- const page1Response = makeApiResponse(
- Array.from({ length: 10 }, (_, i) => ({ id: `r${i}` })),
- { pages: 2 },
- );
- const page1Adapted = Array.from({ length: 10 }, (_, i) =>
- fakeResource(`r${i}`),
- );
-
- const page2Response = makeApiResponse(
- Array.from({ length: 5 }, (_, i) => ({ id: `r${10 + i}` })),
- { pages: 2 },
- );
-
- const refreshPage1Response = makeApiResponse([{ id: "r-fresh-1" }], {
- pages: 1,
- });
- const refreshPage1Adapted = [fakeResource("r-fresh-1")];
-
- // page 2 hangs until we explicitly resolve it
- let resolveNextPage: (v: unknown) => void = () => {};
- const hangingPage2 = new Promise((r) => {
- resolveNextPage = r;
- });
-
- let callCount = 0;
- findingGroupActionsMock.getLatestFindingGroupResources.mockImplementation(
- (args: { page: number }) => {
- callCount++;
- if (callCount === 1) {
- return Promise.resolve(page1Response);
- }
- if (args.page === 2) {
- return hangingPage2;
- }
- return Promise.resolve(refreshPage1Response);
- },
- );
-
- findingGroupActionsMock.adaptFindingGroupResourcesResponse
- .mockReturnValueOnce(page1Adapted)
- .mockReturnValue(refreshPage1Adapted);
-
- const onSetResources = vi.fn();
- const onAppendResources = vi.fn();
-
- // When — mount and wait for page 1
- const { result } = renderHook(() =>
- useInfiniteResources(
- defaultOptions({ onSetResources, onAppendResources }),
- ),
- );
- await flushAsync();
-
- expect(onSetResources).toHaveBeenCalledWith(page1Adapted, true);
-
- // Trigger loadNextPage (increments pageRef to 2 in buggy code)
- const sentinel = document.createElement("div");
- act(() => {
- result.current.sentinelRef(sentinel);
- });
- act(() => {
- triggerIntersection();
- });
- // Do NOT flush — page 2 is hanging in-flight
-
- // Refresh fires while page 2 is in-flight
- act(() => {
- result.current.refresh();
- });
- await flushAsync();
-
- // Resolve hanging page 2 after refresh (simulates late stale response)
- await act(async () => {
- resolveNextPage(page2Response);
- await new Promise((r) => setTimeout(r, 0));
- });
-
- // Then — the aborted page 2 must NOT deliver resources (signal.aborted check)
- expect(onAppendResources).not.toHaveBeenCalled();
-
- // The refresh must have fetched page 1 and delivered fresh resources
- expect(onSetResources).toHaveBeenCalledWith(refreshPage1Adapted, false);
-
- // The refresh call must request page=1 (not page=3 due to stale pageRef)
- // Exact call sequence: [0]=initial page 1, [1]=loadNextPage page 2, [2]=refresh page 1
- const calls =
- findingGroupActionsMock.getLatestFindingGroupResources.mock.calls;
- expect((calls[0][0] as { page: number }).page).toBe(1); // initial fetch
- expect((calls[1][0] as { page: number }).page).toBe(2); // loadNextPage
- expect((calls[2][0] as { page: number }).page).toBe(1); // refresh
- });
-
- it("should fetch sequential pages without skipping when loadNextPage is used normally", async () => {
- // Given — page 1 has 3 pages; pages load sequentially
- const makePageResponse = (startIdx: number, total: number) =>
- makeApiResponse(
- Array.from({ length: 5 }, (_, i) => ({ id: `r${startIdx + i}` })),
- { pages: total },
- );
-
- findingGroupActionsMock.getLatestFindingGroupResources
- .mockResolvedValueOnce(makePageResponse(0, 3)) // page 1
- .mockResolvedValueOnce(makePageResponse(5, 3)) // page 2
- .mockResolvedValueOnce(makePageResponse(10, 3)); // page 3
-
- findingGroupActionsMock.adaptFindingGroupResourcesResponse
- .mockReturnValueOnce(
- Array.from({ length: 5 }, (_, i) => fakeResource(`r${i}`)),
- )
- .mockReturnValueOnce(
- Array.from({ length: 5 }, (_, i) => fakeResource(`r${5 + i}`)),
- )
- .mockReturnValueOnce(
- Array.from({ length: 5 }, (_, i) => fakeResource(`r${10 + i}`)),
- );
-
- const onAppendResources = vi.fn();
-
- // When — mount and wait for page 1
- const { result } = renderHook(() =>
- useInfiniteResources(defaultOptions({ onAppendResources })),
- );
- await flushAsync();
-
- // Attach sentinel
- const sentinel = document.createElement("div");
- act(() => {
- result.current.sentinelRef(sentinel);
- });
-
- // Load page 2
- act(() => {
- triggerIntersection();
- });
- await flushAsync();
-
- // Load page 3
- act(() => {
- triggerIntersection();
- });
- await flushAsync();
-
- // Then — pages were fetched in order: 2, 3 (not 2, 4 due to double-increment)
- const calls =
- findingGroupActionsMock.getLatestFindingGroupResources.mock.calls;
- expect(calls[1][0].page).toBe(2);
- expect(calls[2][0].page).toBe(3);
- });
- });
-});
diff --git a/ui/lib/compliance/score-utils.ts b/ui/lib/compliance/score-utils.ts
index f4daf388e6..7c1aaf4275 100644
--- a/ui/lib/compliance/score-utils.ts
+++ b/ui/lib/compliance/score-utils.ts
@@ -53,3 +53,9 @@ export function getScoreLabel(score: number): string {
if (score >= SCORE_THRESHOLDS.WARNING) return "Moderate Risk";
return "Critical Risk";
}
+
+export function getScoreIndicatorClass(variant: ScoreColorVariant): string {
+ if (variant === "danger") return "bg-bg-fail";
+ if (variant === "warning") return "bg-bg-warning";
+ return "bg-bg-pass";
+}
diff --git a/ui/lib/date-utils.test.ts b/ui/lib/date-utils.test.ts
new file mode 100644
index 0000000000..894506ffd1
--- /dev/null
+++ b/ui/lib/date-utils.test.ts
@@ -0,0 +1,35 @@
+import { format, parseISO } from "date-fns";
+import { describe, expect, it } from "vitest";
+
+import { toLocalDateString } from "./date-utils";
+
+describe("toLocalDateString", () => {
+ it("returns undefined for nullish or empty input", () => {
+ expect(toLocalDateString(undefined)).toBeUndefined();
+ expect(toLocalDateString(null)).toBeUndefined();
+ expect(toLocalDateString("")).toBeUndefined();
+ });
+
+ it("returns undefined for malformed strings", () => {
+ expect(toLocalDateString("not-a-date")).toBeUndefined();
+ });
+
+ it("returns undefined for invalid Date instances", () => {
+ expect(toLocalDateString(new Date("not-a-date"))).toBeUndefined();
+ });
+
+ it("formats an ISO string in the user's local timezone", () => {
+ // Near UTC midnight — the UTC split ("2026-04-19") differs from the local
+ // date for any tz with a positive offset. We pin parity with date-fns so
+ // the assertion holds regardless of where CI runs.
+ const iso = "2026-04-19T23:15:00Z";
+ const expected = format(parseISO(iso), "yyyy-MM-dd");
+
+ expect(toLocalDateString(iso)).toBe(expected);
+ });
+
+ it("formats a Date instance using its local calendar day", () => {
+ const date = new Date(2026, 3, 20, 10, 0, 0); // April 20, 2026 local
+ expect(toLocalDateString(date)).toBe("2026-04-20");
+ });
+});
diff --git a/ui/lib/date-utils.ts b/ui/lib/date-utils.ts
index 40717b4e14..7bce896580 100644
--- a/ui/lib/date-utils.ts
+++ b/ui/lib/date-utils.ts
@@ -1,4 +1,26 @@
-import { formatDistanceToNow } from "date-fns";
+import { format, formatDistanceToNow, parseISO } from "date-fns";
+
+/**
+ * Formats an ISO string or Date into a `yyyy-MM-dd` string in the user's local
+ * timezone. Mirrors the format used by `DateWithTime`, so UI chips/URLs built
+ * with this helper match what the user sees in tables and pickers. Returns
+ * undefined for null, empty, or malformed input so callers can guard on it
+ * (e.g. `isDisabled={!toLocalDateString(x)}`). Do NOT use this for UTC-based
+ * date bucketing (e.g. chart axes partitioned server-side by UTC day) — that
+ * use case needs a separate UTC helper.
+ */
+export function toLocalDateString(
+ value: string | Date | null | undefined,
+): string | undefined {
+ if (!value) return undefined;
+ try {
+ const date = typeof value === "string" ? parseISO(value) : value;
+ if (isNaN(date.getTime())) return undefined;
+ return format(date, "yyyy-MM-dd");
+ } catch {
+ return undefined;
+ }
+}
/**
* Formats a duration in seconds to a human-readable string like "2h 5m 30s".
diff --git a/ui/lib/finding-detail.ts b/ui/lib/finding-detail.ts
new file mode 100644
index 0000000000..69c0224297
--- /dev/null
+++ b/ui/lib/finding-detail.ts
@@ -0,0 +1,62 @@
+import { createDict } from "@/lib";
+import type { FindingProps } from "@/types/components";
+import type { FindingResourceRow } from "@/types/findings-table";
+import type { ProviderType } from "@/types/providers";
+
+interface JsonApiFindingResponse {
+ data?: FindingProps;
+ included?: Record[];
+}
+
+export function expandFindingWithRelationships(
+ apiResponse: JsonApiFindingResponse | undefined,
+): FindingProps | null {
+ if (!apiResponse?.data) {
+ return null;
+ }
+
+ const resourceDict = createDict("resources", apiResponse);
+ const scanDict = createDict("scans", apiResponse);
+ const providerDict = createDict("providers", apiResponse);
+
+ const finding = apiResponse.data;
+ const scan = scanDict[finding.relationships?.scan?.data?.id];
+ const resource =
+ resourceDict[finding.relationships?.resources?.data?.[0]?.id];
+ const provider = providerDict[scan?.relationships?.provider?.data?.id];
+
+ return {
+ ...finding,
+ relationships: { ...finding.relationships, scan, resource, provider },
+ } as FindingProps;
+}
+
+export function findingToFindingResourceRow(
+ finding: FindingProps,
+): FindingResourceRow {
+ const resource = finding.relationships?.resource?.attributes;
+ const provider = finding.relationships?.provider?.attributes;
+
+ return {
+ id: finding.id,
+ rowType: "resource",
+ findingId: finding.id,
+ checkId: finding.attributes.check_id,
+ providerType: (provider?.provider || "aws") as ProviderType,
+ providerAlias: provider?.alias || "-",
+ providerUid: provider?.uid || "-",
+ resourceName: resource?.name || "-",
+ resourceType: resource?.type || "-",
+ resourceGroup: "-",
+ resourceUid: resource?.uid || "-",
+ service: resource?.service || "-",
+ region: resource?.region || "-",
+ severity: finding.attributes.severity,
+ status: finding.attributes.status,
+ delta: finding.attributes.delta,
+ isMuted: finding.attributes.muted,
+ mutedReason: finding.attributes.muted_reason,
+ firstSeenAt: finding.attributes.first_seen_at,
+ lastSeenAt: finding.attributes.updated_at,
+ };
+}
diff --git a/ui/lib/findings-groups.test.ts b/ui/lib/findings-groups.test.ts
index 9fa73c5b1c..1d319371de 100644
--- a/ui/lib/findings-groups.test.ts
+++ b/ui/lib/findings-groups.test.ts
@@ -3,9 +3,11 @@ import { describe, expect, it } from "vitest";
import type { FindingGroupRow } from "@/types";
import {
+ canDrillDownFindingGroup,
getActiveStatusFilter,
getFilteredFindingGroupDelta,
getFindingGroupDelta,
+ getFindingGroupImpactedCounts,
isFindingGroupMuted,
} from "./findings-groups";
@@ -138,6 +140,119 @@ describe("getActiveStatusFilter", () => {
});
});
+describe("getFindingGroupImpactedCounts", () => {
+ it("should fall back to pass and fail counts when resources total is zero", () => {
+ // Given
+ const group = makeGroup({
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ failCount: 3,
+ passCount: 2,
+ muted: false,
+ });
+
+ // When
+ const result = getFindingGroupImpactedCounts(group);
+
+ // Then
+ expect(result).toEqual({ impacted: 3, total: 5 });
+ });
+
+ it("should include manual findings in fallback counts when resources total is zero", () => {
+ // Given
+ const group = makeGroup({
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ failCount: 3,
+ passCount: 2,
+ manualCount: 4,
+ muted: false,
+ });
+
+ // When
+ const result = getFindingGroupImpactedCounts(group);
+
+ // Then
+ expect(result).toEqual({ impacted: 3, total: 9 });
+ });
+
+ it("should include muted pass and fail counts in the denominator when the result is muted", () => {
+ // Given
+ const group = makeGroup({
+ resourcesTotal: 0,
+ resourcesFail: 0,
+ failCount: 3,
+ passCount: 2,
+ failMutedCount: 4,
+ passMutedCount: 1,
+ muted: true,
+ });
+
+ // When
+ const result = getFindingGroupImpactedCounts(group);
+
+ // Then
+ expect(result).toEqual({ impacted: 3, total: 10 });
+ });
+
+ it("should keep resource-based counts when resources total is available", () => {
+ // Given
+ const group = makeGroup({
+ resourcesTotal: 6,
+ resourcesFail: 4,
+ failCount: 2,
+ passCount: 1,
+ failMutedCount: 5,
+ passMutedCount: 3,
+ muted: true,
+ });
+
+ // When
+ const result = getFindingGroupImpactedCounts(group);
+
+ // Then
+ expect(result).toEqual({ impacted: 4, total: 6 });
+ });
+});
+
+describe("canDrillDownFindingGroup", () => {
+ it("should allow drill-down when resources exist", () => {
+ expect(
+ canDrillDownFindingGroup(
+ makeGroup({
+ resourcesTotal: 2,
+ failCount: 0,
+ }),
+ ),
+ ).toBe(true);
+ });
+
+ it("should keep zero-resource fallback groups non-expandable even when fallback counts are present", () => {
+ expect(
+ canDrillDownFindingGroup(
+ makeGroup({
+ resourcesTotal: 0,
+ failCount: 0,
+ passCount: 2,
+ manualCount: 1,
+ }),
+ ),
+ ).toBe(false);
+ });
+
+ it("should keep drill-down disabled for zero-resource groups when the displayed total is zero", () => {
+ expect(
+ canDrillDownFindingGroup(
+ makeGroup({
+ resourcesTotal: 0,
+ failCount: 0,
+ passCount: 0,
+ }),
+ ),
+ ).toBe(false);
+ });
+});
+
describe("getFilteredFindingGroupDelta", () => {
it("falls back to the aggregate delta when no status filter is active", () => {
expect(
diff --git a/ui/lib/findings-groups.ts b/ui/lib/findings-groups.ts
index 7f15e9ad2c..60f1911c05 100644
--- a/ui/lib/findings-groups.ts
+++ b/ui/lib/findings-groups.ts
@@ -1,9 +1,10 @@
-import type { FindingGroupRow } from "@/types";
-
-type FindingGroupMutedState = Pick<
- FindingGroupRow,
- "muted" | "mutedCount" | "resourcesFail" | "resourcesTotal"
->;
+import {
+ FINDING_DELTA,
+ FINDING_STATUS,
+ type FindingDelta,
+ type FindingGroupRow,
+ type FindingStatus,
+} from "@/types";
type FindingGroupDeltaState = Pick<
FindingGroupRow,
@@ -23,7 +24,18 @@ type FindingGroupDeltaState = Pick<
| "changedManualMutedCount"
>;
-export function isFindingGroupMuted(group: FindingGroupMutedState): boolean {
+type FindingGroupDelta = Exclude;
+
+type FindingGroupStatus = FindingStatus;
+
+const FINDING_GROUP_STATUSES = Object.values(FINDING_STATUS);
+
+export function isFindingGroupMuted(
+ group: Pick<
+ FindingGroupRow,
+ "muted" | "mutedCount" | "resourcesFail" | "resourcesTotal"
+ >,
+): boolean {
if (typeof group.muted === "boolean") {
return group.muted;
}
@@ -38,6 +50,54 @@ export function isFindingGroupMuted(group: FindingGroupMutedState): boolean {
);
}
+export function getFindingGroupImpactedCounts(
+ group: Pick<
+ FindingGroupRow,
+ | "resourcesTotal"
+ | "resourcesFail"
+ | "passCount"
+ | "failCount"
+ | "manualCount"
+ | "passMutedCount"
+ | "failMutedCount"
+ | "manualMutedCount"
+ | "muted"
+ | "mutedCount"
+ >,
+): { impacted: number; total: number } {
+ if (group.resourcesTotal > 0) {
+ return {
+ impacted: group.resourcesFail,
+ total: group.resourcesTotal,
+ };
+ }
+
+ const total =
+ (group.passCount ?? 0) + (group.failCount ?? 0) + (group.manualCount ?? 0);
+
+ if (!isFindingGroupMuted(group)) {
+ return {
+ impacted: group.failCount ?? 0,
+ total,
+ };
+ }
+
+ return {
+ impacted: group.failCount ?? 0,
+ total:
+ total +
+ (group.passMutedCount ?? 0) +
+ (group.failMutedCount ?? 0) +
+ (group.manualMutedCount ?? 0),
+ };
+}
+
+export function canDrillDownFindingGroup(
+ group: Pick,
+): boolean {
+ return group.resourcesTotal > 0;
+}
+
function getNewDeltaTotal(group: FindingGroupDeltaState): number {
const breakdownTotal =
(group.newFailCount ?? 0) +
@@ -64,21 +124,18 @@ function getChangedDeltaTotal(group: FindingGroupDeltaState): number {
export function getFindingGroupDelta(
group: FindingGroupDeltaState,
-): "new" | "changed" | "none" {
+): FindingGroupDelta {
if (getNewDeltaTotal(group) > 0) {
- return "new";
+ return FINDING_DELTA.NEW;
}
if (getChangedDeltaTotal(group) > 0) {
- return "changed";
+ return FINDING_DELTA.CHANGED;
}
- return "none";
+ return FINDING_DELTA.NONE;
}
-const FINDING_GROUP_STATUSES = ["FAIL", "PASS", "MANUAL"] as const;
-type FindingGroupStatus = (typeof FINDING_GROUP_STATUSES)[number];
-
type FindingGroupFiltersRecord = Record;
function parseStatusFilterValue(
@@ -142,13 +199,13 @@ function getNewDeltaForStatuses(
statuses: Set,
): number {
let total = 0;
- if (statuses.has("FAIL")) {
+ if (statuses.has(FINDING_STATUS.FAIL)) {
total += (group.newFailCount ?? 0) + (group.newFailMutedCount ?? 0);
}
- if (statuses.has("PASS")) {
+ if (statuses.has(FINDING_STATUS.PASS)) {
total += (group.newPassCount ?? 0) + (group.newPassMutedCount ?? 0);
}
- if (statuses.has("MANUAL")) {
+ if (statuses.has(FINDING_STATUS.MANUAL)) {
total += (group.newManualCount ?? 0) + (group.newManualMutedCount ?? 0);
}
return total;
@@ -159,13 +216,13 @@ function getChangedDeltaForStatuses(
statuses: Set,
): number {
let total = 0;
- if (statuses.has("FAIL")) {
+ if (statuses.has(FINDING_STATUS.FAIL)) {
total += (group.changedFailCount ?? 0) + (group.changedFailMutedCount ?? 0);
}
- if (statuses.has("PASS")) {
+ if (statuses.has(FINDING_STATUS.PASS)) {
total += (group.changedPassCount ?? 0) + (group.changedPassMutedCount ?? 0);
}
- if (statuses.has("MANUAL")) {
+ if (statuses.has(FINDING_STATUS.MANUAL)) {
total +=
(group.changedManualCount ?? 0) + (group.changedManualMutedCount ?? 0);
}
@@ -182,7 +239,7 @@ function getChangedDeltaForStatuses(
export function getFilteredFindingGroupDelta(
group: FindingGroupDeltaState,
filters: FindingGroupFiltersRecord,
-): "new" | "changed" | "none" {
+): FindingGroupDelta {
const activeStatuses = getActiveStatusFilter(filters);
if (!activeStatuses || !hasAnyDeltaBreakdown(group)) {
@@ -190,12 +247,12 @@ export function getFilteredFindingGroupDelta(
}
if (getNewDeltaForStatuses(group, activeStatuses) > 0) {
- return "new";
+ return FINDING_DELTA.NEW;
}
if (getChangedDeltaForStatuses(group, activeStatuses) > 0) {
- return "changed";
+ return FINDING_DELTA.CHANGED;
}
- return "none";
+ return FINDING_DELTA.NONE;
}
diff --git a/ui/lib/findings-scan-filters.test.ts b/ui/lib/findings-scan-filters.test.ts
index fcf32b507d..a1e4d4a4e0 100644
--- a/ui/lib/findings-scan-filters.test.ts
+++ b/ui/lib/findings-scan-filters.test.ts
@@ -50,7 +50,7 @@ describe("resolveFindingScanDateFilters", () => {
{
id: "scan-1",
attributes: {
- inserted_at: "2026-04-07T10:00:00Z",
+ completed_at: "2026-04-07T10:00:00Z",
},
},
],
@@ -68,7 +68,7 @@ describe("resolveFindingScanDateFilters", () => {
const loadScan = vi.fn().mockResolvedValue({
id: "scan-2",
attributes: {
- inserted_at: "2026-04-05T08:00:00Z",
+ completed_at: "2026-04-05T08:00:00Z",
},
});
@@ -97,7 +97,7 @@ describe("resolveFindingScanDateFilters", () => {
{
id: "scan-1",
attributes: {
- inserted_at: "2026-04-07T10:00:00Z",
+ completed_at: "2026-04-07T10:00:00Z",
},
},
],
diff --git a/ui/lib/findings-scan-filters.ts b/ui/lib/findings-scan-filters.ts
index dfbb1bc44c..a5984cb141 100644
--- a/ui/lib/findings-scan-filters.ts
+++ b/ui/lib/findings-scan-filters.ts
@@ -1,7 +1,11 @@
interface ScanDateSource {
id: string;
attributes?: {
- inserted_at?: string;
+ // Findings are persisted when the scan finishes, so their `inserted_at`
+ // aligns with the scan's `completed_at` — not the scan's `inserted_at`
+ // (which is when the scan row was first created and can fall on a
+ // different UTC day for scans that cross midnight).
+ completed_at?: string;
};
}
@@ -34,10 +38,10 @@ function hasInsertedAtFilter(filters: Record): boolean {
}
export function buildFindingScanDateFilters(
- scanInsertedAtValues: string[],
+ scanCompletedAtValues: string[],
): Record {
const dates = Array.from(
- new Set(scanInsertedAtValues.map(formatScanDate).filter(Boolean)),
+ new Set(scanCompletedAtValues.map(formatScanDate).filter(Boolean)),
).sort() as string[];
if (dates.length === 0) {
@@ -82,11 +86,11 @@ export async function resolveFindingScanDateFilters({
});
}
- const scanInsertedAtValues = scanIds
- .map((scanId) => scansById.get(scanId)?.attributes?.inserted_at)
- .filter((insertedAt): insertedAt is string => Boolean(insertedAt));
+ const scanCompletedAtValues = scanIds
+ .map((scanId) => scansById.get(scanId)?.attributes?.completed_at)
+ .filter((completedAt): completedAt is string => Boolean(completedAt));
- const dateFilters = buildFindingScanDateFilters(scanInsertedAtValues);
+ const dateFilters = buildFindingScanDateFilters(scanCompletedAtValues);
if (Object.keys(dateFilters).length === 0) {
return filters;
diff --git a/ui/lib/helper-filters.test.ts b/ui/lib/helper-filters.test.ts
index fcff4a85d8..858a95e68f 100644
--- a/ui/lib/helper-filters.test.ts
+++ b/ui/lib/helper-filters.test.ts
@@ -1,16 +1,39 @@
import { describe, expect, it } from "vitest";
+import type { ScanEntity } from "@/types/scans";
+
import {
+ getScanEntityLabel,
hasDateFilter,
hasDateOrScanFilter,
hasHistoricalFindingFilter,
} from "./helper-filters";
+function makeScan(overrides: Partial = {}): ScanEntity {
+ return {
+ id: "scan-1",
+ providerInfo: {
+ provider: "aws",
+ alias: "Production",
+ uid: "123456789012",
+ },
+ attributes: {
+ name: "Nightly scan",
+ completed_at: "2026-04-07T10:00:00Z",
+ },
+ ...overrides,
+ };
+}
+
describe("hasDateOrScanFilter", () => {
it("returns true for scan filters", () => {
expect(hasDateOrScanFilter({ "filter[scan__in]": "scan-1" })).toBe(true);
});
+ it("returns true for exact scan filters", () => {
+ expect(hasDateOrScanFilter({ "filter[scan]": "scan-1" })).toBe(true);
+ });
+
it("returns true for inserted_at filters", () => {
expect(
hasDateOrScanFilter({ "filter[inserted_at__gte]": "2026-04-01" }),
@@ -30,6 +53,43 @@ describe("hasDateFilter", () => {
});
});
+describe("getScanEntityLabel", () => {
+ it("combines provider display name and scan name with a dash", () => {
+ expect(getScanEntityLabel(makeScan())).toBe("AWS - Nightly scan");
+ });
+
+ it("uses the provider type even when the account alias is present", () => {
+ // Guard against regressions where alias/uid leak back into the label.
+ expect(
+ getScanEntityLabel(
+ makeScan({
+ providerInfo: {
+ provider: "azure",
+ alias: "Production",
+ uid: "subscription-xyz",
+ },
+ }),
+ ),
+ ).toBe("Azure - Nightly scan");
+ });
+
+ it("renders the provider display name for non-AWS providers", () => {
+ expect(
+ getScanEntityLabel(makeScan({ providerInfo: { provider: "gcp" } })),
+ ).toBe("Google Cloud - Nightly scan");
+ });
+
+ it("returns only the provider name when the scan name is missing", () => {
+ expect(
+ getScanEntityLabel(
+ makeScan({
+ attributes: { name: "", completed_at: "2026-04-07T10:00:00Z" },
+ }),
+ ),
+ ).toBe("AWS");
+ });
+});
+
describe("hasHistoricalFindingFilter", () => {
it("returns true for inserted_at filters", () => {
expect(
@@ -43,6 +103,10 @@ describe("hasHistoricalFindingFilter", () => {
);
});
+ it("returns true for exact scan filters", () => {
+ expect(hasHistoricalFindingFilter({ "filter[scan]": "scan-1" })).toBe(true);
+ });
+
it("returns false when neither date nor scan filters are active", () => {
expect(
hasHistoricalFindingFilter({ "filter[provider_type__in]": "aws" }),
diff --git a/ui/lib/helper-filters.ts b/ui/lib/helper-filters.ts
index 7280deb53d..ae510bdc83 100644
--- a/ui/lib/helper-filters.ts
+++ b/ui/lib/helper-filters.ts
@@ -1,6 +1,10 @@
import { ProviderProps, ProvidersApiResponse, ScanProps } from "@/types";
import { FilterEntity } from "@/types/filters";
-import { GroupFilterEntity, ProviderConnectionStatus } from "@/types/providers";
+import {
+ getProviderDisplayName,
+ GroupFilterEntity,
+ ProviderConnectionStatus,
+} from "@/types/providers";
import { ScanEntity } from "@/types/scans";
/**
@@ -31,7 +35,10 @@ export const extractFiltersAndQuery = (
*/
export const hasDateOrScanFilter = (searchParams: Record) =>
Object.keys(searchParams).some(
- (key) => key.includes("inserted_at") || key.includes("scan__in"),
+ (key) =>
+ key.includes("inserted_at") ||
+ key.includes("scan__in") ||
+ key === "filter[scan]",
);
/**
@@ -96,6 +103,22 @@ export const isScanEntity = (entity: ScanEntity) => {
return entity && entity.providerInfo && entity.attributes;
};
+/**
+ * Canonical human label for a scan entity: "{Provider name} - {scan name}".
+ * Provider name comes from `getProviderDisplayName` (e.g. "AWS", "Google Cloud"),
+ * never the account alias/uid — those identify the account, not the provider.
+ * Shared by the findings filter chips and the multi-select trigger badge so
+ * both surfaces stay in sync. Returns the provider name alone when the scan
+ * name is empty, or the scan name alone if the provider type doesn't resolve.
+ */
+export function getScanEntityLabel(scan: ScanEntity): string {
+ const providerLabel = getProviderDisplayName(scan.providerInfo.provider);
+ const scanName = scan.attributes.name || "";
+
+ if (providerLabel && scanName) return `${providerLabel} - ${scanName}`;
+ return providerLabel || scanName;
+}
+
/**
* Creates a scan details mapping for filters from completed scans.
* Used to provide detailed information for scan filters in the UI.
diff --git a/ui/lib/helper.ts b/ui/lib/helper.ts
index a8f8d24b38..315fcde3f1 100644
--- a/ui/lib/helper.ts
+++ b/ui/lib/helper.ts
@@ -216,6 +216,10 @@ export const downloadComplianceCsv = async (
complianceId: string,
toast: ReturnType["toast"],
): Promise => {
+ toast({
+ title: "Download Started",
+ description: "Preparing the CSV report. This may take a moment.",
+ });
const result = await getComplianceCsv(scanId, complianceId);
await downloadFile(
result,
@@ -236,8 +240,12 @@ export const downloadComplianceReportPdf = async (
reportType: ComplianceReportType,
toast: ReturnType["toast"],
): Promise => {
- const result = await getCompliancePdfReport(scanId, reportType);
const reportName = COMPLIANCE_REPORT_DISPLAY_NAMES[reportType];
+ toast({
+ title: "Download Started",
+ description: `Preparing the ${reportName} PDF report. This may take a moment.`,
+ });
+ const result = await getCompliancePdfReport(scanId, reportType);
await downloadFile(
result,
"application/pdf",
diff --git a/ui/lib/invitation-routing.ts b/ui/lib/invitation-routing.ts
deleted file mode 100644
index 85f132d922..0000000000
--- a/ui/lib/invitation-routing.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Query param name + value used to bypass the backward-compat redirect
- * in proxy.ts when the user explicitly chose "Create an account"
- * from the invitation smart router.
- *
- * Client sends: /sign-up?invitation_token=…&action=signup
- * Proxy skips redirect when "action" param is present.
- */
-export const INVITATION_ACTION_PARAM = "action";
-export const INVITATION_SIGNUP_ACTION = "signup";
diff --git a/ui/package.json b/ui/package.json
index 74d6d15fc6..8b6890c068 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -42,7 +42,7 @@
"@langchain/mcp-adapters": "1.1.3",
"@langchain/openai": "1.1.3",
"@lezer/highlight": "1.2.3",
- "@next/third-parties": "16.1.6",
+ "@next/third-parties": "16.2.3",
"@radix-ui/react-alert-dialog": "1.1.14",
"@radix-ui/react-avatar": "1.1.11",
"@radix-ui/react-checkbox": "1.3.3",
@@ -52,6 +52,7 @@
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.7",
"@radix-ui/react-popover": "1.1.15",
+ "@radix-ui/react-progress": "1.1.7",
"@radix-ui/react-radio-group": "1.3.8",
"@radix-ui/react-scroll-area": "1.2.10",
"@radix-ui/react-select": "2.2.5",
@@ -93,13 +94,13 @@
"lucide-react": "0.543.0",
"marked": "15.0.12",
"nanoid": "5.1.6",
- "next": "16.1.6",
+ "next": "16.2.3",
"next-auth": "5.0.0-beta.30",
"next-themes": "0.2.1",
"radix-ui": "1.4.2",
- "react": "19.2.4",
+ "react": "19.2.5",
"react-day-picker": "9.13.0",
- "react-dom": "19.2.4",
+ "react-dom": "19.2.5",
"react-hook-form": "7.62.0",
"react-markdown": "10.1.0",
"recharts": "2.15.4",
@@ -122,7 +123,7 @@
"devDependencies": {
"@eslint/eslintrc": "3.3.3",
"@iconify/react": "5.2.1",
- "@next/eslint-plugin-next": "16.1.6",
+ "@next/eslint-plugin-next": "16.2.3",
"@playwright/test": "1.56.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
@@ -143,7 +144,7 @@
"babel-plugin-react-compiler": "1.0.0",
"dotenv-expand": "12.0.3",
"eslint": "9.39.2",
- "eslint-config-next": "16.1.6",
+ "eslint-config-next": "16.2.3",
"eslint-config-prettier": "10.1.5",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2",
@@ -169,12 +170,12 @@
"overrides": {
"@react-types/shared": "3.26.0",
"@internationalized/date": "3.10.0",
- "alert>react": "19.2.4",
- "alert>react-dom": "19.2.4",
- "@react-aria/ssr>react": "19.2.4",
- "@react-aria/ssr>react-dom": "19.2.4",
- "@react-aria/visually-hidden>react": "19.2.4",
- "@react-aria/interactions>react": "19.2.4",
+ "alert>react": "19.2.5",
+ "alert>react-dom": "19.2.5",
+ "@react-aria/ssr>react": "19.2.5",
+ "@react-aria/ssr>react-dom": "19.2.5",
+ "@react-aria/visually-hidden>react": "19.2.5",
+ "@react-aria/interactions>react": "19.2.5",
"lodash": "4.17.23",
"lodash-es": "4.17.23",
"hono": "4.12.4",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 967b017d64..d9cf769380 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -7,12 +7,12 @@ settings:
overrides:
'@react-types/shared': 3.26.0
'@internationalized/date': 3.10.0
- alert>react: 19.2.4
- alert>react-dom: 19.2.4
- '@react-aria/ssr>react': 19.2.4
- '@react-aria/ssr>react-dom': 19.2.4
- '@react-aria/visually-hidden>react': 19.2.4
- '@react-aria/interactions>react': 19.2.4
+ alert>react: 19.2.5
+ alert>react-dom: 19.2.5
+ '@react-aria/ssr>react': 19.2.5
+ '@react-aria/ssr>react-dom': 19.2.5
+ '@react-aria/visually-hidden>react': 19.2.5
+ '@react-aria/interactions>react': 19.2.5
lodash: 4.17.23
lodash-es: 4.17.23
hono: 4.12.4
@@ -34,7 +34,7 @@ importers:
dependencies:
'@ai-sdk/react':
specifier: 2.0.111
- version: 2.0.111(react@19.2.4)(zod@4.1.11)
+ version: 2.0.111(react@19.2.5)(zod@4.1.11)
'@aws-sdk/client-bedrock-runtime':
specifier: 3.988.0
version: 3.988.0
@@ -58,10 +58,10 @@ importers:
version: 7.1.7
'@heroui/react':
specifier: 2.8.4
- version: 2.8.4(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18)
+ version: 2.8.4(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.1.18)
'@hookform/resolvers':
specifier: 5.2.2
- version: 5.2.2(react-hook-form@7.62.0(react@19.2.4))
+ version: 5.2.2(react-hook-form@7.62.0(react@19.2.5))
'@internationalized/date':
specifier: 3.10.0
version: 3.10.0
@@ -73,7 +73,7 @@ importers:
version: 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))
'@langchain/mcp-adapters':
specifier: 1.1.3
- version: 1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11))
+ version: 1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11))
'@langchain/openai':
specifier: 1.1.3
version: 1.1.3(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(ws@8.19.0)
@@ -81,83 +81,86 @@ importers:
specifier: 1.2.3
version: 1.2.3
'@next/third-parties':
- specifier: 16.1.6
- version: 16.1.6(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
+ specifier: 16.2.3
+ version: 16.2.3(next@16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)
'@radix-ui/react-alert-dialog':
specifier: 1.1.14
- version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-avatar':
specifier: 1.1.11
- version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-checkbox':
specifier: 1.3.3
- version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-collapsible':
specifier: 1.1.12
- version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-dialog':
specifier: 1.1.14
- version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-dropdown-menu':
specifier: 2.1.15
- version: 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-icons':
specifier: 1.3.2
- version: 1.3.2(react@19.2.4)
+ version: 1.3.2(react@19.2.5)
'@radix-ui/react-label':
specifier: 2.1.7
- version: 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-popover':
specifier: 1.1.15
- version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-progress':
+ specifier: 1.1.7
+ version: 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-radio-group':
specifier: 1.3.8
- version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-scroll-area':
specifier: 1.2.10
- version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-select':
specifier: 2.2.5
- version: 2.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 2.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-separator':
specifier: 1.1.7
- version: 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-slot':
specifier: 1.2.3
- version: 1.2.3(@types/react@19.2.8)(react@19.2.4)
+ version: 1.2.3(@types/react@19.2.8)(react@19.2.5)
'@radix-ui/react-tabs':
specifier: 1.1.13
- version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-toast':
specifier: 1.2.14
- version: 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-tooltip':
specifier: 1.2.8
- version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-use-controllable-state':
specifier: 1.2.2
- version: 1.2.2(@types/react@19.2.8)(react@19.2.4)
+ version: 1.2.2(@types/react@19.2.8)(react@19.2.5)
'@react-aria/i18n':
specifier: 3.12.13
- version: 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@react-aria/ssr':
specifier: 3.9.4
- version: 3.9.4(react@19.2.4)
+ version: 3.9.4(react@19.2.5)
'@react-aria/visually-hidden':
specifier: 3.8.12
- version: 3.8.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 3.8.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@react-stately/utils':
specifier: 3.10.8
- version: 3.10.8(react@19.2.4)
+ version: 3.10.8(react@19.2.5)
'@react-types/datepicker':
specifier: 3.13.2
- version: 3.13.2(react@19.2.4)
+ version: 3.13.2(react@19.2.5)
'@react-types/shared':
specifier: 3.26.0
- version: 3.26.0(react@19.2.4)
+ version: 3.26.0(react@19.2.5)
'@sentry/nextjs':
specifier: 10.27.0
- version: 10.27.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.104.1)
+ version: 10.27.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.104.1)
'@tailwindcss/postcss':
specifier: 4.1.18
version: 4.1.18
@@ -166,7 +169,7 @@ importers:
version: 0.5.16(tailwindcss@4.1.18)
'@tanstack/react-table':
specifier: 8.21.3
- version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 8.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@types/dagre':
specifier: 0.7.53
version: 0.7.53
@@ -175,13 +178,13 @@ importers:
version: 4.0.9
'@uiw/react-codemirror':
specifier: 4.25.8
- version: 4.25.8(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 4.25.8(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
ai:
specifier: 5.0.109
version: 5.0.109(zod@4.1.11)
alert:
specifier: 6.0.2
- version: 6.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 6.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
class-variance-authority:
specifier: 0.7.1
version: 0.7.1
@@ -190,7 +193,7 @@ importers:
version: 2.1.1
cmdk:
specifier: 1.1.1
- version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
codemirror:
specifier: 6.0.2
version: 6.0.2
@@ -205,7 +208,7 @@ importers:
version: 4.1.0
framer-motion:
specifier: 11.18.2
- version: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
import-in-the-middle:
specifier: 2.0.0
version: 2.0.0
@@ -223,10 +226,10 @@ importers:
version: 4.0.0
langchain:
specifier: 1.2.10
- version: 1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))
+ version: 1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.1(zod@4.1.11))
lucide-react:
specifier: 0.543.0
- version: 0.543.0(react@19.2.4)
+ version: 0.543.0(react@19.2.5)
marked:
specifier: 15.0.12
version: 15.0.12
@@ -234,35 +237,35 @@ importers:
specifier: 5.1.6
version: 5.1.6
next:
- specifier: 16.1.6
- version: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ specifier: 16.2.3
+ version: 16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
next-auth:
specifier: 5.0.0-beta.30
- version: 5.0.0-beta.30(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
+ version: 5.0.0-beta.30(next@16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)
next-themes:
specifier: 0.2.1
- version: 0.2.1(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 0.2.1(next@16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
radix-ui:
specifier: 1.4.2
- version: 1.4.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.4.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react:
- specifier: 19.2.4
- version: 19.2.4
+ specifier: 19.2.5
+ version: 19.2.5
react-day-picker:
specifier: 9.13.0
- version: 9.13.0(react@19.2.4)
+ version: 9.13.0(react@19.2.5)
react-dom:
- specifier: 19.2.4
- version: 19.2.4(react@19.2.4)
+ specifier: 19.2.5
+ version: 19.2.5(react@19.2.5)
react-hook-form:
specifier: 7.62.0
- version: 7.62.0(react@19.2.4)
+ version: 7.62.0(react@19.2.5)
react-markdown:
specifier: 10.1.0
- version: 10.1.0(@types/react@19.2.8)(react@19.2.4)
+ version: 10.1.0(@types/react@19.2.8)(react@19.2.5)
recharts:
specifier: 2.15.4
- version: 2.15.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 2.15.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
require-in-the-middle:
specifier: 8.0.1
version: 8.0.1
@@ -277,7 +280,7 @@ importers:
version: 3.20.0
streamdown:
specifier: 1.6.10
- version: 1.6.10(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.4)
+ version: 1.6.10(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.5)
tailwind-merge:
specifier: 3.3.1
version: 3.3.1
@@ -292,13 +295,13 @@ importers:
version: 1.4.0
use-stick-to-bottom:
specifier: 1.1.1
- version: 1.1.1(react@19.2.4)
+ version: 1.1.1(react@19.2.5)
uuid:
specifier: 11.1.0
version: 11.1.0
vaul:
specifier: 1.1.2
- version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
world-atlas:
specifier: 2.0.2
version: 2.0.2
@@ -307,17 +310,17 @@ importers:
version: 4.1.11
zustand:
specifier: 5.0.8
- version: 5.0.8(@types/react@19.2.8)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
+ version: 5.0.8(@types/react@19.2.8)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
devDependencies:
'@eslint/eslintrc':
specifier: 3.3.3
version: 3.3.3
'@iconify/react':
specifier: 5.2.1
- version: 5.2.1(react@19.2.4)
+ version: 5.2.1(react@19.2.5)
'@next/eslint-plugin-next':
- specifier: 16.1.6
- version: 16.1.6
+ specifier: 16.2.3
+ version: 16.2.3
'@playwright/test':
specifier: 1.56.1
version: 1.56.1
@@ -326,7 +329,7 @@ importers:
version: 6.9.1
'@testing-library/react':
specifier: 16.3.2
- version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@testing-library/user-event':
specifier: 14.6.1
version: 14.6.1(@testing-library/dom@10.4.1)
@@ -379,14 +382,14 @@ importers:
specifier: 9.39.2
version: 9.39.2(jiti@2.6.1)
eslint-config-next:
- specifier: 16.1.6
- version: 16.1.6(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4)
+ specifier: 16.2.3
+ version: 16.2.3(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4)
eslint-config-prettier:
specifier: 10.1.5
version: 10.1.5(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import:
specifier: 2.32.0
- version: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
+ version: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.0)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y:
specifier: 6.10.2
version: 6.10.2(eslint@9.39.2(jiti@2.6.1))
@@ -2266,66 +2269,66 @@ packages:
'@ndaidong/bellajs@12.0.1':
resolution: {integrity: sha512-1iY42uiHz0cxNMbde7O3zVN+ZX1viOOUOBRt6ht6lkRZbSjwOnFV34Zv4URp3hGzEe6L9Byk7BOq/41H0PzAOQ==}
- '@next/env@16.1.6':
- resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
+ '@next/env@16.2.3':
+ resolution: {integrity: sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==}
- '@next/eslint-plugin-next@16.1.6':
- resolution: {integrity: sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==}
+ '@next/eslint-plugin-next@16.2.3':
+ resolution: {integrity: sha512-nE/b9mht28XJxjTwKs/yk7w4XTaU3t40UHVAky6cjiijdP/SEy3hGsnQMPxmXPTpC7W4/97okm6fngKnvCqVaA==}
- '@next/swc-darwin-arm64@16.1.6':
- resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==}
+ '@next/swc-darwin-arm64@16.2.3':
+ resolution: {integrity: sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@16.1.6':
- resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==}
+ '@next/swc-darwin-x64@16.2.3':
+ resolution: {integrity: sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@16.1.6':
- resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==}
+ '@next/swc-linux-arm64-gnu@16.2.3':
+ resolution: {integrity: sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-arm64-musl@16.1.6':
- resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==}
+ '@next/swc-linux-arm64-musl@16.2.3':
+ resolution: {integrity: sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@next/swc-linux-x64-gnu@16.1.6':
- resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==}
+ '@next/swc-linux-x64-gnu@16.2.3':
+ resolution: {integrity: sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-x64-musl@16.1.6':
- resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==}
+ '@next/swc-linux-x64-musl@16.2.3':
+ resolution: {integrity: sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@next/swc-win32-arm64-msvc@16.1.6':
- resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==}
+ '@next/swc-win32-arm64-msvc@16.2.3':
+ resolution: {integrity: sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.1.6':
- resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==}
+ '@next/swc-win32-x64-msvc@16.2.3':
+ resolution: {integrity: sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
- '@next/third-parties@16.1.6':
- resolution: {integrity: sha512-/cLY1egaH529ylSMSK+C8dA3nWDLL4hOFR4fca9OLWWxjcNwzsbuq2pPb/tmdWL9Zj3K1nTjd1pWQoSlaDQ0VA==}
+ '@next/third-parties@16.2.3':
+ resolution: {integrity: sha512-SLXsH3CYR0JRbBg5+WFoyjTiQh4cbxfszcafgFdAck+AU4g6em/jX9hZfyfvCgsjIkk0OGkTetrHqKDSNhxxuA==}
peerDependencies:
next: ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0-beta.0
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
@@ -3568,13 +3571,13 @@ packages:
'@react-aria/interactions@3.25.5':
resolution: {integrity: sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==}
peerDependencies:
- react: 19.2.4
+ react: 19.2.5
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-aria/interactions@3.26.0':
resolution: {integrity: sha512-AAEcHiltjfbmP1i9iaVw34Mb7kbkiHpYdqieWufldh4aplWgsF11YQZOfaCJW4QoR2ML4Zzoa9nfFwLXA52R7Q==}
peerDependencies:
- react: 19.2.4
+ react: 19.2.5
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-aria/label@3.7.21':
@@ -3686,13 +3689,13 @@ packages:
resolution: {integrity: sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==}
engines: {node: '>= 12'}
peerDependencies:
- react: 19.2.4
+ react: 19.2.5
'@react-aria/ssr@3.9.4':
resolution: {integrity: sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==}
engines: {node: '>= 12'}
peerDependencies:
- react: 19.2.4
+ react: 19.2.5
'@react-aria/switch@3.7.7':
resolution: {integrity: sha512-auV3g1qh+d/AZk7Idw2BOcYeXfCD9iDaiGmlcLJb9Eaz4nkq8vOkQxIXQFrn9Xhb+PfQzmQYKkt5N6P2ZNsw/g==}
@@ -3763,18 +3766,18 @@ packages:
'@react-aria/visually-hidden@3.8.12':
resolution: {integrity: sha512-Bawm+2Cmw3Xrlr7ARzl2RLtKh0lNUdJ0eNqzWcyx4c0VHUAWtThmH5l+HRqFUGzzutFZVo89SAy40BAbd0gjVw==}
peerDependencies:
- react: 19.2.4
+ react: 19.2.5
'@react-aria/visually-hidden@3.8.27':
resolution: {integrity: sha512-hD1DbL3WnjPnCdlQjwe19bQVRAGJyN0Aaup+s7NNtvZUn7AjoEH78jo8TE+L8yM7z/OZUQF26laCfYqeIwWn4g==}
peerDependencies:
- react: 19.2.4
+ react: 19.2.5
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-aria/visually-hidden@3.8.29':
resolution: {integrity: sha512-1joCP+MHBLd+YA6Gb08nMFfDBhOF0Kh1gR1SA8zoxEB5RMfQEEkufIB8k0GGwvHGSCK3gFyO8UAVsD0+rRYEyg==}
peerDependencies:
- react: 19.2.4
+ react: 19.2.5
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-stately/calendar@3.8.4':
@@ -5388,8 +5391,8 @@ packages:
alert@6.0.2:
resolution: {integrity: sha512-Oi8u2HRNN6mzpjgKGii2Uuf9iOhyfbeUAHH/5MwnVmC8DS9GrEBjZBFpoavkNj+ZKnBr/Lqx+6YKLDKrggKfPA==}
peerDependencies:
- react: 19.2.4
- react-dom: 19.2.4
+ react: 19.2.5
+ react-dom: 19.2.5
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@@ -5518,6 +5521,11 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ baseline-browser-mapping@2.10.19:
+ resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
baseline-browser-mapping@2.9.15:
resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==}
hasBin: true
@@ -6211,8 +6219,8 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
- eslint-config-next@16.1.6:
- resolution: {integrity: sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==}
+ eslint-config-next@16.2.3:
+ resolution: {integrity: sha512-Dnkrylzjof/Az7iNoIQJqD18zTxQZcngir19KJaiRsMnnjpQSVoa6aEg/1Q4hQC+cW90uTlgQYadwL1CYNwFWA==}
peerDependencies:
eslint: '>=9.0.0'
typescript: '>=3.3.1'
@@ -6229,8 +6237,8 @@ packages:
eslint-import-resolver-node@0.3.9:
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
- eslint-import-resolver-typescript@3.10.1:
- resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==}
+ eslint-import-resolver-typescript@3.10.0:
+ resolution: {integrity: sha512-aV3/dVsT0/H9BtpNwbaqvl+0xGMRGzncLyhm793NFGvbwGGvzyAykqWZ8oZlZuGwuHkwJjhWJkG1cM3ynvd2pQ==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
eslint: '*'
@@ -7745,8 +7753,8 @@ packages:
react: '*'
react-dom: '*'
- next@16.1.6:
- resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==}
+ next@16.2.3:
+ resolution: {integrity: sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -8243,10 +8251,10 @@ packages:
peerDependencies:
react: '>=16.8.0'
- react-dom@19.2.4:
- resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
+ react-dom@19.2.5:
+ resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
peerDependencies:
- react: ^19.2.4
+ react: ^19.2.5
react-hook-form@7.62.0:
resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==}
@@ -8321,8 +8329,8 @@ packages:
react: '>=16.6.0'
react-dom: '>=16.6.0'
- react@19.2.4:
- resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
+ react@19.2.5:
+ resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
engines: {node: '>=0.10.0'}
readdirp@3.6.0:
@@ -9474,12 +9482,12 @@ snapshots:
dependencies:
json-schema: 0.4.0
- '@ai-sdk/react@2.0.111(react@19.2.4)(zod@4.1.11)':
+ '@ai-sdk/react@2.0.111(react@19.2.5)(zod@4.1.11)':
dependencies:
'@ai-sdk/provider-utils': 3.0.18(zod@4.1.11)
ai: 5.0.109(zod@4.1.11)
- react: 19.2.4
- swr: 2.3.8(react@19.2.4)
+ react: 19.2.5
+ swr: 2.3.8(react@19.2.5)
throttleit: 2.1.0
optionalDependencies:
zod: 4.1.11
@@ -10818,11 +10826,11 @@ snapshots:
'@floating-ui/core': 1.7.3
'@floating-ui/utils': 0.2.10
- '@floating-ui/react-dom@2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@floating-ui/react-dom@2.1.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@floating-ui/dom': 1.7.4
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
'@floating-ui/utils@0.2.10': {}
@@ -10852,814 +10860,814 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@heroui/accordion@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/accordion@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
- '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-aria-accordion': 2.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/tree': 3.9.2(react@19.2.4)
- '@react-types/accordion': 3.0.0-alpha.26(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-aria-accordion': 2.2.17(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/tree': 3.9.2(react@19.2.5)
+ '@react-types/accordion': 3.0.0-alpha.26(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/alert@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/alert@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- framer-motion
- '@heroui/aria-utils@2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/aria-utils@2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/collections': 3.12.7(react@19.2.4)
- '@react-types/overlays': 3.9.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/collections': 3.12.7(react@19.2.5)
+ '@react-types/overlays': 3.9.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- '@heroui/theme'
- framer-motion
- '@heroui/autocomplete@2.3.28(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/autocomplete@2.3.28(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/input': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/input': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- '@react-aria/combobox': 3.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/combobox': 3.11.1(react@19.2.4)
- '@react-types/combobox': 3.13.8(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ '@react-aria/combobox': 3.13.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/combobox': 3.11.1(react@19.2.5)
+ '@react-types/combobox': 3.13.8(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- '@heroui/avatar@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/avatar@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-image': 2.1.12(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-image': 2.1.12(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/badge@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/badge@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/breadcrumbs@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/breadcrumbs@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-aria/breadcrumbs': 3.5.28(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/breadcrumbs': 3.7.16(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/breadcrumbs': 3.5.28(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/breadcrumbs': 3.7.16(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/button@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/button@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-aria-button': 2.2.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/calendar@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/calendar@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
- '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/use-aria-button': 2.2.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@internationalized/date': 3.10.0
- '@react-aria/calendar': 3.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/calendar': 3.8.4(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/button': 3.14.0(react@19.2.4)
- '@react-types/calendar': 3.7.4(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/calendar': 3.9.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/calendar': 3.8.4(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/button': 3.14.0(react@19.2.5)
+ '@react-types/calendar': 3.7.4(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
scroll-into-view-if-needed: 3.0.10
- '@heroui/card@2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/card@2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-aria-button': 2.2.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/checkbox@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/checkbox@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-callback-ref': 2.1.8(react@19.2.4)
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- '@react-aria/checkbox': 3.16.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/checkbox': 3.7.1(react@19.2.4)
- '@react-stately/toggle': 3.9.1(react@19.2.4)
- '@react-types/checkbox': 3.10.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-callback-ref': 2.1.8(react@19.2.5)
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ '@react-aria/checkbox': 3.16.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/checkbox': 3.7.1(react@19.2.5)
+ '@react-stately/toggle': 3.9.1(react@19.2.5)
+ '@react-types/checkbox': 3.10.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/chip@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/chip@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/code@2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/code@2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4)
+ '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/date-input@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/date-input@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
'@internationalized/date': 3.10.0
- '@react-aria/datepicker': 3.15.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/datepicker': 3.15.1(react@19.2.4)
- '@react-types/datepicker': 3.13.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/datepicker': 3.15.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/datepicker': 3.15.1(react@19.2.5)
+ '@react-types/datepicker': 3.13.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/date-picker@2.3.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/date-picker@2.3.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/calendar': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/date-input': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/calendar': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/date-input': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
'@internationalized/date': 3.10.0
- '@react-aria/datepicker': 3.15.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/datepicker': 3.15.1(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/datepicker': 3.13.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/datepicker': 3.15.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/datepicker': 3.15.1(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/datepicker': 3.13.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/divider@2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/divider@2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-rsc-utils': 2.1.9(react@19.2.4)
- '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4)
+ '@heroui/react-rsc-utils': 2.1.9(react@19.2.5)
+ '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/dom-animation@2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
+ '@heroui/dom-animation@2.1.10(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
dependencies:
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@heroui/drawer@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/drawer@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/modal': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/modal': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- framer-motion
- '@heroui/dropdown@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/dropdown@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/menu': 2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/menu': 2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/menu': 3.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/menu': 3.9.7(react@19.2.4)
- '@react-types/menu': 3.10.4(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/menu': 3.19.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/menu': 3.9.7(react@19.2.5)
+ '@react-types/menu': 3.10.4(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/form@2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/form@2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-stately/form': 3.2.1(react@19.2.4)
- '@react-types/form': 3.7.15(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-stately/form': 3.2.1(react@19.2.5)
+ '@react-types/form': 3.7.15(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/framer-utils@2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/framer-utils@2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-measure': 2.1.8(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-measure': 2.1.8(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- '@heroui/theme'
- '@heroui/image@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/image@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-image': 2.1.12(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-image': 2.1.12(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/input-otp@2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/input-otp@2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-form-reset': 2.0.1(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/form': 3.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/form': 3.2.1(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/textfield': 3.12.5(react@19.2.4)
- input-otp: 1.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-form-reset': 2.0.1(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/form': 3.1.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/form': 3.2.1(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/textfield': 3.12.5(react@19.2.5)
+ input-otp: 1.4.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/input@2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/input@2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/textfield': 3.18.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/textfield': 3.12.5(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- react-textarea-autosize: 8.5.9(@types/react@19.2.8)(react@19.2.4)
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/textfield': 3.18.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/textfield': 3.12.5(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-textarea-autosize: 8.5.9(@types/react@19.2.8)(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- '@heroui/kbd@2.2.21(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/kbd@2.2.21(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4)
+ '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/link@2.2.22(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/link@2.2.22(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-aria-link': 2.2.20(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/link': 3.6.4(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-aria-link': 2.2.20(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/link': 3.6.4(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/listbox@2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/listbox@2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-is-mobile': 2.2.12(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/listbox': 3.14.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/list': 3.13.0(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@tanstack/react-virtual': 3.11.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-is-mobile': 2.2.12(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/listbox': 3.14.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/list': 3.13.0(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@tanstack/react-virtual': 3.11.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- framer-motion
- '@heroui/menu@2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/menu@2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-is-mobile': 2.2.12(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/menu': 3.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/tree': 3.9.2(react@19.2.4)
- '@react-types/menu': 3.10.4(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-is-mobile': 2.2.12(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/menu': 3.19.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/tree': 3.9.2(react@19.2.5)
+ '@react-types/menu': 3.10.4(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- framer-motion
- '@heroui/modal@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/modal@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
- '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-aria-modal-overlay': 2.2.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-disclosure': 2.2.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-draggable': 2.1.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-viewport-size': 2.0.1(react@19.2.4)
- '@react-aria/dialog': 3.5.29(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/overlays': 3.6.19(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-aria-button': 2.2.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-aria-modal-overlay': 2.2.18(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-disclosure': 2.2.16(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-draggable': 2.1.17(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-viewport-size': 2.0.1(react@19.2.5)
+ '@react-aria/dialog': 3.5.29(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/overlays': 3.6.19(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/navbar@2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/navbar@2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
- '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-resize': 2.1.8(react@19.2.4)
- '@heroui/use-scroll-position': 2.1.8(react@19.2.4)
- '@react-aria/button': 3.14.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/toggle': 3.9.1(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-resize': 2.1.8(react@19.2.5)
+ '@heroui/use-scroll-position': 2.1.8(react@19.2.5)
+ '@react-aria/button': 3.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/toggle': 3.9.1(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/number-input@2.0.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/number-input@2.0.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/numberfield': 3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/numberfield': 3.10.1(react@19.2.4)
- '@react-types/button': 3.14.0(react@19.2.4)
- '@react-types/numberfield': 3.8.14(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/numberfield': 3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/numberfield': 3.10.1(react@19.2.5)
+ '@react-types/button': 3.14.0(react@19.2.5)
+ '@react-types/numberfield': 3.8.14(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- framer-motion
- '@heroui/pagination@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/pagination@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-intersection-observer': 2.2.14(react@19.2.4)
- '@heroui/use-pagination': 2.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-intersection-observer': 2.2.14(react@19.2.5)
+ '@heroui/use-pagination': 2.2.17(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
scroll-into-view-if-needed: 3.0.10
- '@heroui/popover@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/popover@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
- '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-aria-overlay': 2.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- '@react-aria/dialog': 3.5.29(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/overlays': 3.6.19(react@19.2.4)
- '@react-types/overlays': 3.9.1(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-aria-button': 2.2.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-aria-overlay': 2.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ '@react-aria/dialog': 3.5.29(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/overlays': 3.6.19(react@19.2.5)
+ '@react-types/overlays': 3.9.1(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/progress@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/progress@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-is-mounted': 2.1.8(react@19.2.4)
- '@react-aria/progress': 3.4.26(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/progress': 3.5.15(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-is-mounted': 2.1.8(react@19.2.5)
+ '@react-aria/progress': 3.4.26(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/progress': 3.5.15(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/radio@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/radio@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/radio': 3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/radio': 3.11.1(react@19.2.4)
- '@react-types/radio': 3.9.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/radio': 3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/radio': 3.11.1(react@19.2.5)
+ '@react-types/radio': 3.9.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/react-rsc-utils@2.1.9(react@19.2.4)':
+ '@heroui/react-rsc-utils@2.1.9(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/react-utils@2.1.13(react@19.2.4)':
+ '@heroui/react-utils@2.1.13(react@19.2.5)':
dependencies:
- '@heroui/react-rsc-utils': 2.1.9(react@19.2.4)
+ '@heroui/react-rsc-utils': 2.1.9(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- react: 19.2.4
+ react: 19.2.5
- '@heroui/react@2.8.4(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18)':
+ '@heroui/react@2.8.4(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.1.18)':
dependencies:
- '@heroui/accordion': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/alert': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/autocomplete': 2.3.28(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/avatar': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/badge': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/breadcrumbs': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/calendar': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/card': 2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/checkbox': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/chip': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/code': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/date-input': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/date-picker': 2.3.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/drawer': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/dropdown': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/image': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/input': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/input-otp': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/kbd': 2.2.21(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/link': 2.2.22(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/menu': 2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/modal': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/navbar': 2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/number-input': 2.0.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/pagination': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/progress': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/radio': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/select': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/skeleton': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/slider': 2.4.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/snippet': 2.2.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/spacer': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/switch': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/table': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/tabs': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/accordion': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/alert': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/autocomplete': 2.3.28(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/avatar': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/badge': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/breadcrumbs': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/calendar': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/card': 2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/checkbox': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/chip': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/code': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/date-input': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/date-picker': 2.3.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/drawer': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/dropdown': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/image': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/input': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/input-otp': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/kbd': 2.2.21(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/link': 2.2.22(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/menu': 2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/modal': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/navbar': 2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/number-input': 2.0.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/pagination': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/progress': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/radio': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/select': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/skeleton': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/slider': 2.4.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/snippet': 2.2.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/spacer': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/switch': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/table': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/tabs': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/toast': 2.0.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/user': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/toast': 2.0.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/user': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- tailwindcss
- '@heroui/ripple@2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/ripple@2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/scroll-shadow@2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/scroll-shadow@2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-data-scroll-overflow': 2.2.12(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-data-scroll-overflow': 2.2.12(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/select@2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/select@2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-aria-multiselect': 2.4.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-form-reset': 2.0.1(react@19.2.4)
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/form': 3.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-aria-button': 2.2.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-aria-multiselect': 2.4.18(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-form-reset': 2.0.1(react@19.2.5)
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/form': 3.1.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/shared-icons@2.1.10(react@19.2.4)':
+ '@heroui/shared-icons@2.1.10(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
'@heroui/shared-utils@2.1.11': {}
- '@heroui/skeleton@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/skeleton@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/slider@2.4.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/slider@2.4.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/slider': 3.8.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/slider': 3.7.1(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/slider': 3.8.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/slider': 3.7.1(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- framer-motion
- '@heroui/snippet@2.2.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/snippet@2.2.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-clipboard': 2.1.9(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-clipboard': 2.1.9(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/spacer@2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/spacer@2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4)
+ '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/spinner@2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/spinner@2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- framer-motion
- '@heroui/switch@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/switch@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/switch': 3.7.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/toggle': 3.9.1(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/switch': 3.7.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/toggle': 3.9.1(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/system-rsc@2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4)':
+ '@heroui/system-rsc@2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.5)':
dependencies:
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-types/shared': 3.26.0(react@19.2.5)
clsx: 1.2.1
- react: 19.2.4
+ react: 19.2.5
- '@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4)
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.5)
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- '@heroui/theme'
- '@heroui/table@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/table@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/checkbox': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/checkbox': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/spacer': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/spacer': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/table': 3.17.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/table': 3.15.0(react@19.2.4)
- '@react-stately/virtualizer': 4.4.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/grid': 3.3.5(react@19.2.4)
- '@react-types/table': 3.13.3(react@19.2.4)
- '@tanstack/react-virtual': 3.11.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/table': 3.17.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/table': 3.15.0(react@19.2.5)
+ '@react-stately/virtualizer': 4.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/grid': 3.3.5(react@19.2.5)
+ '@react-types/table': 3.13.3(react@19.2.5)
+ '@tanstack/react-virtual': 3.11.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/tabs@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/tabs@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-is-mounted': 2.1.8(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/tabs': 3.10.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/tabs': 3.8.5(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-is-mounted': 2.1.8(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/tabs': 3.10.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/tabs': 3.8.5(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
scroll-into-view-if-needed: 3.0.10
'@heroui/theme@2.4.22(tailwindcss@4.1.18)':
@@ -11674,212 +11682,212 @@ snapshots:
tailwind-variants: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.18)
tailwindcss: 4.1.18
- '@heroui/toast@2.0.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/toast@2.0.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/shared-icons': 2.1.10(react@19.2.4)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/shared-icons': 2.1.10(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-is-mobile': 2.2.12(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/toast': 3.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/toast': 3.1.2(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-is-mobile': 2.2.12(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/toast': 3.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/toast': 3.1.2(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/tooltip@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/tooltip@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
- '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@heroui/use-aria-overlay': 2.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/tooltip': 3.8.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/tooltip': 3.5.7(react@19.2.4)
- '@react-types/overlays': 3.9.1(react@19.2.4)
- '@react-types/tooltip': 3.4.20(react@19.2.4)
- framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-aria-overlay': 2.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ '@react-aria/overlays': 3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/tooltip': 3.8.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/tooltip': 3.5.7(react@19.2.5)
+ '@react-types/overlays': 3.9.1(react@19.2.5)
+ '@react-types/tooltip': 3.4.20(react@19.2.5)
+ framer-motion: 11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/use-aria-accordion@2.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/use-aria-accordion@2.2.17(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/button': 3.14.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/selection': 3.25.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/tree': 3.9.2(react@19.2.4)
- '@react-types/accordion': 3.0.0-alpha.26(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-aria/button': 3.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/selection': 3.25.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/tree': 3.9.2(react@19.2.5)
+ '@react-types/accordion': 3.0.0-alpha.26(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
transitivePeerDependencies:
- react-dom
- '@heroui/use-aria-button@2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/use-aria-button@2.2.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/button': 3.14.0(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/button': 3.14.0(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
transitivePeerDependencies:
- react-dom
- '@heroui/use-aria-link@2.2.20(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/use-aria-link@2.2.20(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/link': 3.6.4(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/link': 3.6.4(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
transitivePeerDependencies:
- react-dom
- '@heroui/use-aria-modal-overlay@2.2.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/use-aria-modal-overlay@2.2.18(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/use-aria-overlay': 2.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/overlays': 3.6.19(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@heroui/use-aria-overlay': 2.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/overlays': 3.6.19(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/use-aria-multiselect@2.4.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/use-aria-multiselect@2.4.18(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/listbox': 3.14.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/menu': 3.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/selection': 3.25.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/form': 3.2.1(react@19.2.4)
- '@react-stately/list': 3.13.0(react@19.2.4)
- '@react-stately/menu': 3.9.7(react@19.2.4)
- '@react-types/button': 3.14.0(react@19.2.4)
- '@react-types/overlays': 3.9.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.21(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/listbox': 3.14.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/menu': 3.19.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/selection': 3.25.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/form': 3.2.1(react@19.2.5)
+ '@react-stately/list': 3.13.0(react@19.2.5)
+ '@react-stately/menu': 3.9.7(react@19.2.5)
+ '@react-types/button': 3.14.0(react@19.2.5)
+ '@react-types/overlays': 3.9.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/use-aria-overlay@2.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/use-aria-overlay@2.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@heroui/use-callback-ref@2.1.8(react@19.2.4)':
+ '@heroui/use-callback-ref@2.1.8(react@19.2.5)':
dependencies:
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- react: 19.2.4
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ react: 19.2.5
- '@heroui/use-clipboard@2.1.9(react@19.2.4)':
+ '@heroui/use-clipboard@2.1.9(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/use-data-scroll-overflow@2.2.12(react@19.2.4)':
+ '@heroui/use-data-scroll-overflow@2.2.12(react@19.2.5)':
dependencies:
'@heroui/shared-utils': 2.1.11
- react: 19.2.4
+ react: 19.2.5
- '@heroui/use-disclosure@2.2.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/use-disclosure@2.2.16(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/use-callback-ref': 2.1.8(react@19.2.4)
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- react: 19.2.4
+ '@heroui/use-callback-ref': 2.1.8(react@19.2.5)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ react: 19.2.5
transitivePeerDependencies:
- react-dom
- '@heroui/use-draggable@2.1.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/use-draggable@2.1.17(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
transitivePeerDependencies:
- react-dom
- '@heroui/use-form-reset@2.0.1(react@19.2.4)':
+ '@heroui/use-form-reset@2.0.1(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/use-image@2.1.12(react@19.2.4)':
+ '@heroui/use-image@2.1.12(react@19.2.5)':
dependencies:
- '@heroui/react-utils': 2.1.13(react@19.2.4)
- '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4)
- react: 19.2.4
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
+ '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.5)
+ react: 19.2.5
- '@heroui/use-intersection-observer@2.2.14(react@19.2.4)':
+ '@heroui/use-intersection-observer@2.2.14(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/use-is-mobile@2.2.12(react@19.2.4)':
+ '@heroui/use-is-mobile@2.2.12(react@19.2.5)':
dependencies:
- '@react-aria/ssr': 3.9.10(react@19.2.4)
- react: 19.2.4
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
+ react: 19.2.5
- '@heroui/use-is-mounted@2.1.8(react@19.2.4)':
+ '@heroui/use-is-mounted@2.1.8(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/use-measure@2.1.8(react@19.2.4)':
+ '@heroui/use-measure@2.1.8(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/use-pagination@2.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/use-pagination@2.2.17(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@heroui/shared-utils': 2.1.11
- '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
+ '@react-aria/i18n': 3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
transitivePeerDependencies:
- react-dom
- '@heroui/use-resize@2.1.8(react@19.2.4)':
+ '@heroui/use-resize@2.1.8(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/use-safe-layout-effect@2.1.8(react@19.2.4)':
+ '@heroui/use-safe-layout-effect@2.1.8(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/use-scroll-position@2.1.8(react@19.2.4)':
+ '@heroui/use-scroll-position@2.1.8(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/use-viewport-size@2.0.1(react@19.2.4)':
+ '@heroui/use-viewport-size@2.0.1(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@heroui/user@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@heroui/user@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@heroui/avatar': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@heroui/react-utils': 2.1.13(react@19.2.4)
+ '@heroui/avatar': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@heroui/react-utils': 2.1.13(react@19.2.5)
'@heroui/shared-utils': 2.1.11
- '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@heroui/theme': 2.4.22(tailwindcss@4.1.18)
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
'@hono/node-server@1.19.10(hono@4.12.4)':
dependencies:
hono: 4.12.4
- '@hookform/resolvers@5.2.2(react-hook-form@7.62.0(react@19.2.4))':
+ '@hookform/resolvers@5.2.2(react-hook-form@7.62.0(react@19.2.5))':
dependencies:
'@standard-schema/utils': 0.3.0
- react-hook-form: 7.62.0(react@19.2.4)
+ react-hook-form: 7.62.0(react@19.2.5)
'@humanfs/core@0.19.1': {}
@@ -11892,10 +11900,10 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
- '@iconify/react@5.2.1(react@19.2.4)':
+ '@iconify/react@5.2.1(react@19.2.5)':
dependencies:
'@iconify/types': 2.0.0
- react: 19.2.4
+ react: 19.2.5
'@iconify/types@2.0.0': {}
@@ -12188,21 +12196,21 @@ snapshots:
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))
uuid: 10.0.0
- '@langchain/langgraph-sdk@1.5.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@langchain/langgraph-sdk@1.5.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
p-queue: 9.1.0
p-retry: 7.1.1
uuid: 13.0.0
optionalDependencies:
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11)':
+ '@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11)':
dependencies:
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))
'@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))
- '@langchain/langgraph-sdk': 1.5.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@langchain/langgraph-sdk': 1.5.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
uuid: 10.0.0
zod: 4.1.11
optionalDependencies:
@@ -12211,10 +12219,10 @@ snapshots:
- react
- react-dom
- '@langchain/mcp-adapters@1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11))':
+ '@langchain/mcp-adapters@1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11))':
dependencies:
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))
- '@langchain/langgraph': 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11)
+ '@langchain/langgraph': 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11)
'@modelcontextprotocol/sdk': 1.26.0(@cfworker/json-schema@4.1.1)(zod@4.1.11)
debug: 4.4.3
zod: 4.1.11
@@ -12315,40 +12323,40 @@ snapshots:
'@ndaidong/bellajs@12.0.1': {}
- '@next/env@16.1.6': {}
+ '@next/env@16.2.3': {}
- '@next/eslint-plugin-next@16.1.6':
+ '@next/eslint-plugin-next@16.2.3':
dependencies:
fast-glob: 3.3.1
- '@next/swc-darwin-arm64@16.1.6':
+ '@next/swc-darwin-arm64@16.2.3':
optional: true
- '@next/swc-darwin-x64@16.1.6':
+ '@next/swc-darwin-x64@16.2.3':
optional: true
- '@next/swc-linux-arm64-gnu@16.1.6':
+ '@next/swc-linux-arm64-gnu@16.2.3':
optional: true
- '@next/swc-linux-arm64-musl@16.1.6':
+ '@next/swc-linux-arm64-musl@16.2.3':
optional: true
- '@next/swc-linux-x64-gnu@16.1.6':
+ '@next/swc-linux-x64-gnu@16.2.3':
optional: true
- '@next/swc-linux-x64-musl@16.1.6':
+ '@next/swc-linux-x64-musl@16.2.3':
optional: true
- '@next/swc-win32-arm64-msvc@16.1.6':
+ '@next/swc-win32-arm64-msvc@16.2.3':
optional: true
- '@next/swc-win32-x64-msvc@16.1.6':
+ '@next/swc-win32-x64-msvc@16.2.3':
optional: true
- '@next/third-parties@16.1.6(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
+ '@next/third-parties@16.2.3(next@16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)':
dependencies:
- next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
+ next: 16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
third-party-capital: 1.0.20
'@noble/ciphers@1.3.0': {}
@@ -12645,2048 +12653,2048 @@ snapshots:
'@radix-ui/primitive@1.1.3': {}
- '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-accordion@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-accordion@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-context': 1.1.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-context': 1.1.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-checkbox@1.3.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-checkbox@1.3.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-collapsible@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-collapsible@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-context-menu@2.2.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-context-menu@2.2.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-context@1.1.2(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-context@1.1.3(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-context@1.1.3(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
aria-hidden: 1.2.6
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-direction@1.1.1(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-focus-guards@1.1.2(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-focus-guards@1.1.2(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-form@0.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-form@0.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-hover-card@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-hover-card@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-icons@1.3.2(react@19.2.4)':
+ '@radix-ui/react-icons@1.3.2(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@radix-ui/react-id@1.1.1(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-menu@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-menu@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
aria-hidden: 1.2.6
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-menubar@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-menubar@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-navigation-menu@1.2.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-navigation-menu@1.2.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-one-time-password-field@0.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-one-time-password-field@0.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-password-toggle-field@0.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-password-toggle-field@0.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-popover@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-popover@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
aria-hidden: 1.2.6
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
aria-hidden: 1.2.6
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-popper@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-popper@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@floating-ui/react-dom': 2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
+ '@floating-ui/react-dom': 2.1.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.5)
'@radix-ui/rect': 1.1.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@floating-ui/react-dom': 2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
+ '@floating-ui/react-dom': 2.1.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.5)
'@radix-ui/rect': 1.1.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-presence@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-presence@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-slot': 1.2.4(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.4(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-radio-group@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-radio-group@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-scroll-area@1.2.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-scroll-area@1.2.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-select@2.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-select@2.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
aria-hidden: 1.2.6
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-slider@1.3.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-slider@1.3.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-slot@1.2.3(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-slot@1.2.4(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-slot@1.2.4(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-switch@1.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-switch@1.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-toast@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-toast@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-toggle-group@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-toggle-group@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-toggle': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-toggle': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-toggle@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-toggle@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-toolbar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-toolbar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-toggle-group': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-toggle-group': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
- '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
- use-sync-external-store: 1.6.0(react@19.2.4)
+ react: 19.2.5
+ use-sync-external-store: 1.6.0(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.8)(react@19.2.5)':
dependencies:
'@radix-ui/rect': 1.1.1
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-use-size@1.1.1(@types/react@19.2.8)(react@19.2.4)':
+ '@radix-ui/react-use-size@1.1.1(@types/react@19.2.8)(react@19.2.5)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- react: 19.2.4
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
'@radix-ui/rect@1.1.1': {}
- '@react-aria/breadcrumbs@3.5.28(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/breadcrumbs@3.5.28(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/link': 3.8.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/breadcrumbs': 3.7.16(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/link': 3.8.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/breadcrumbs': 3.7.16(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/button@3.14.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/button@3.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/toolbar': 3.0.0-beta.20(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/toggle': 3.9.1(react@19.2.4)
- '@react-types/button': 3.14.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/toolbar': 3.0.0-beta.20(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/toggle': 3.9.1(react@19.2.5)
+ '@react-types/button': 3.14.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/calendar@3.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/calendar@3.9.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@react-aria/live-announcer': 3.4.4
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/calendar': 3.8.4(react@19.2.4)
- '@react-types/button': 3.14.0(react@19.2.4)
- '@react-types/calendar': 3.7.4(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/calendar': 3.8.4(react@19.2.5)
+ '@react-types/button': 3.14.0(react@19.2.5)
+ '@react-types/calendar': 3.7.4(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/checkbox@3.16.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/checkbox@3.16.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/form': 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/toggle': 3.12.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/checkbox': 3.7.1(react@19.2.4)
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/toggle': 3.9.1(react@19.2.4)
- '@react-types/checkbox': 3.10.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/form': 3.1.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/toggle': 3.12.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/checkbox': 3.7.1(react@19.2.5)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/toggle': 3.9.1(react@19.2.5)
+ '@react-types/checkbox': 3.10.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/combobox@3.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/combobox@3.13.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/listbox': 3.15.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@react-aria/focus': 3.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/listbox': 3.15.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@react-aria/live-announcer': 3.4.4
- '@react-aria/menu': 3.19.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/selection': 3.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/textfield': 3.18.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/combobox': 3.11.1(react@19.2.4)
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-types/button': 3.14.1(react@19.2.4)
- '@react-types/combobox': 3.13.8(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/menu': 3.19.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.31.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/selection': 3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/textfield': 3.18.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/combobox': 3.11.1(react@19.2.5)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-types/button': 3.14.1(react@19.2.5)
+ '@react-types/combobox': 3.13.8(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/datepicker@3.15.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/datepicker@3.15.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
'@internationalized/number': 3.6.5
'@internationalized/string': 3.2.7
- '@react-aria/focus': 3.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/form': 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/spinbutton': 3.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/datepicker': 3.15.1(react@19.2.4)
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-types/button': 3.14.1(react@19.2.4)
- '@react-types/calendar': 3.8.1(react@19.2.4)
- '@react-types/datepicker': 3.13.2(react@19.2.4)
- '@react-types/dialog': 3.5.22(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/focus': 3.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/form': 3.1.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/spinbutton': 3.7.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/datepicker': 3.15.1(react@19.2.5)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-types/button': 3.14.1(react@19.2.5)
+ '@react-types/calendar': 3.8.1(react@19.2.5)
+ '@react-types/datepicker': 3.13.2(react@19.2.5)
+ '@react-types/dialog': 3.5.22(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/dialog@3.5.29(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/dialog@3.5.29(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/dialog': 3.5.22(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/dialog': 3.5.22(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/focus@3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/focus@3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
clsx: 2.1.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/focus@3.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/focus@3.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
clsx: 2.1.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/form@3.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/form@3.1.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/form@3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/form@3.1.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/grid@3.14.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/grid@3.14.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@react-aria/focus': 3.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.14(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@react-aria/live-announcer': 3.4.4
- '@react-aria/selection': 3.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/grid': 3.11.7(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-types/checkbox': 3.10.2(react@19.2.4)
- '@react-types/grid': 3.3.6(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/selection': 3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/grid': 3.11.7(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-types/checkbox': 3.10.2(react@19.2.5)
+ '@react-types/grid': 3.3.6(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/i18n@3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/i18n@3.12.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
'@internationalized/message': 3.1.8
'@internationalized/number': 3.6.5
'@internationalized/string': 3.2.7
- '@react-aria/ssr': 3.9.10(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/i18n@3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/i18n@3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
'@internationalized/message': 3.1.8
'@internationalized/number': 3.6.5
'@internationalized/string': 3.2.7
- '@react-aria/ssr': 3.9.10(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/i18n@3.12.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/i18n@3.12.14(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
'@internationalized/message': 3.1.8
'@internationalized/number': 3.6.5
'@internationalized/string': 3.2.7
- '@react-aria/ssr': 3.9.10(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/interactions@3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/interactions@3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/ssr': 3.9.10(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@react-stately/flags': 3.1.2
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/interactions@3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/interactions@3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/ssr': 3.9.10(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@react-stately/flags': 3.1.2
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/label@3.7.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/label@3.7.21(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/label@3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/label@3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/landmark@3.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/landmark@3.0.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- use-sync-external-store: 1.6.0(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ use-sync-external-store: 1.6.0(react@19.2.5)
- '@react-aria/link@3.8.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/link@3.8.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/link': 3.6.5(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/link': 3.6.5(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/listbox@3.14.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/listbox@3.14.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/selection': 3.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/list': 3.13.0(react@19.2.4)
- '@react-types/listbox': 3.7.4(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/selection': 3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/list': 3.13.0(react@19.2.5)
+ '@react-types/listbox': 3.7.4(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/listbox@3.15.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/listbox@3.15.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/selection': 3.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/list': 3.13.2(react@19.2.4)
- '@react-types/listbox': 3.7.4(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/selection': 3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/list': 3.13.2(react@19.2.5)
+ '@react-types/listbox': 3.7.4(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
'@react-aria/live-announcer@3.4.4':
dependencies:
'@swc/helpers': 0.5.18
- '@react-aria/menu@3.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/menu@3.19.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/selection': 3.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/menu': 3.9.9(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-stately/tree': 3.9.2(react@19.2.4)
- '@react-types/button': 3.14.1(react@19.2.4)
- '@react-types/menu': 3.10.4(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.31.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/selection': 3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/menu': 3.9.9(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-stately/tree': 3.9.2(react@19.2.5)
+ '@react-types/button': 3.14.1(react@19.2.5)
+ '@react-types/menu': 3.10.4(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/menu@3.19.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/menu@3.19.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/overlays': 3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/selection': 3.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/menu': 3.9.9(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-stately/tree': 3.9.4(react@19.2.4)
- '@react-types/button': 3.14.1(react@19.2.4)
- '@react-types/menu': 3.10.5(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/focus': 3.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.14(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/overlays': 3.31.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/selection': 3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/menu': 3.9.9(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-stately/tree': 3.9.4(react@19.2.5)
+ '@react-types/button': 3.14.1(react@19.2.5)
+ '@react-types/menu': 3.10.5(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/numberfield@3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/numberfield@3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/spinbutton': 3.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/textfield': 3.18.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/numberfield': 3.10.1(react@19.2.4)
- '@react-types/button': 3.14.0(react@19.2.4)
- '@react-types/numberfield': 3.8.14(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/spinbutton': 3.7.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/textfield': 3.18.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/numberfield': 3.10.1(react@19.2.5)
+ '@react-types/button': 3.14.0(react@19.2.5)
+ '@react-types/numberfield': 3.8.14(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/overlays@3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/overlays@3.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/ssr': 3.9.10(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/overlays': 3.6.19(react@19.2.4)
- '@react-types/button': 3.14.1(react@19.2.4)
- '@react-types/overlays': 3.9.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/overlays': 3.6.19(react@19.2.5)
+ '@react-types/button': 3.14.1(react@19.2.5)
+ '@react-types/overlays': 3.9.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/overlays@3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/overlays@3.31.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/ssr': 3.9.10(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.29(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/overlays': 3.6.21(react@19.2.4)
- '@react-types/button': 3.14.1(react@19.2.4)
- '@react-types/overlays': 3.9.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/focus': 3.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.14(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.29(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/overlays': 3.6.21(react@19.2.5)
+ '@react-types/button': 3.14.1(react@19.2.5)
+ '@react-types/overlays': 3.9.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/progress@3.4.26(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/progress@3.4.26(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/progress': 3.5.15(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/progress': 3.5.15(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/radio@3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/radio@3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/form': 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/radio': 3.11.1(react@19.2.4)
- '@react-types/radio': 3.9.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/form': 3.1.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/radio': 3.11.1(react@19.2.5)
+ '@react-types/radio': 3.9.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/selection@3.25.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/selection@3.25.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/selection@3.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/selection@3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/focus': 3.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.14(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/slider@3.8.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/slider@3.8.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/slider': 3.7.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/slider': 3.8.2(react@19.2.4)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/slider': 3.7.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/slider': 3.8.2(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/spinbutton@3.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/spinbutton@3.7.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/i18n': 3.12.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@react-aria/i18n': 3.12.14(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@react-aria/live-announcer': 3.4.4
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/button': 3.14.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/button': 3.14.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/ssr@3.9.10(react@19.2.4)':
+ '@react-aria/ssr@3.9.10(react@19.2.5)':
dependencies:
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-aria/ssr@3.9.4(react@19.2.4)':
+ '@react-aria/ssr@3.9.4(react@19.2.5)':
dependencies:
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-aria/switch@3.7.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/switch@3.7.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/toggle': 3.12.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/toggle': 3.9.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/switch': 3.5.15(react@19.2.4)
+ '@react-aria/toggle': 3.12.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/toggle': 3.9.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/switch': 3.5.15(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/table@3.17.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/table@3.17.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/grid': 3.14.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/grid': 3.14.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@react-aria/live-announcer': 3.4.4
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/collections': 3.12.8(react@19.2.4)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
'@react-stately/flags': 3.1.2
- '@react-stately/table': 3.15.0(react@19.2.4)
- '@react-types/checkbox': 3.10.2(react@19.2.4)
- '@react-types/grid': 3.3.5(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/table': 3.13.3(react@19.2.4)
+ '@react-stately/table': 3.15.0(react@19.2.5)
+ '@react-types/checkbox': 3.10.2(react@19.2.5)
+ '@react-types/grid': 3.3.5(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/table': 3.13.3(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/tabs@3.10.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/tabs@3.10.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/selection': 3.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/tabs': 3.8.5(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/tabs': 3.3.20(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/selection': 3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/tabs': 3.8.5(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/tabs': 3.3.20(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/textfield@3.18.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/textfield@3.18.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/form': 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/textfield': 3.12.5(react@19.2.4)
+ '@react-aria/form': 3.1.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/textfield': 3.12.5(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/textfield@3.18.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/textfield@3.18.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/form': 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/label': 3.7.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/utils': 3.11.0(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/textfield': 3.12.6(react@19.2.4)
+ '@react-aria/form': 3.1.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/label': 3.7.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/utils': 3.11.0(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/textfield': 3.12.6(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/toast@3.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/toast@3.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/landmark': 3.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/toast': 3.1.2(react@19.2.4)
- '@react-types/button': 3.14.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/interactions': 3.25.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/landmark': 3.0.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/toast': 3.1.2(react@19.2.5)
+ '@react-types/button': 3.14.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/toggle@3.12.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/toggle@3.12.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/toggle': 3.9.3(react@19.2.4)
- '@react-types/checkbox': 3.10.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/toggle': 3.9.3(react@19.2.5)
+ '@react-types/checkbox': 3.10.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/toolbar@3.0.0-beta.20(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/toolbar@3.0.0-beta.20(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/i18n': 3.12.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/focus': 3.21.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/i18n': 3.12.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/tooltip@3.8.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/tooltip@3.8.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-stately/tooltip': 3.5.7(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/tooltip': 3.4.20(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-stately/tooltip': 3.5.7(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/tooltip': 3.4.20(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/utils@3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/utils@3.30.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/ssr': 3.9.10(react@19.2.4)
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
'@react-stately/flags': 3.1.2
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
clsx: 2.1.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/utils@3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/utils@3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/ssr': 3.9.10(react@19.2.4)
+ '@react-aria/ssr': 3.9.10(react@19.2.5)
'@react-stately/flags': 3.1.2
- '@react-stately/utils': 3.11.0(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/utils': 3.11.0(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
clsx: 2.1.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/visually-hidden@3.8.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/visually-hidden@3.8.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
transitivePeerDependencies:
- react-dom
- '@react-aria/visually-hidden@3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/visually-hidden@3.8.27(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-aria/visually-hidden@3.8.29(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-aria/visually-hidden@3.8.29(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/interactions': 3.26.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/interactions': 3.26.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-stately/calendar@3.8.4(react@19.2.4)':
+ '@react-stately/calendar@3.8.4(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/calendar': 3.7.4(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/calendar': 3.7.4(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/checkbox@3.7.1(react@19.2.4)':
+ '@react-stately/checkbox@3.7.1(react@19.2.5)':
dependencies:
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/checkbox': 3.10.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/checkbox': 3.10.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/collections@3.12.7(react@19.2.4)':
+ '@react-stately/collections@3.12.7(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/collections@3.12.8(react@19.2.4)':
+ '@react-stately/collections@3.12.8(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/combobox@3.11.1(react@19.2.4)':
+ '@react-stately/combobox@3.11.1(react@19.2.5)':
dependencies:
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/list': 3.13.2(react@19.2.4)
- '@react-stately/overlays': 3.6.21(react@19.2.4)
- '@react-stately/select': 3.9.0(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/combobox': 3.13.8(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/list': 3.13.2(react@19.2.5)
+ '@react-stately/overlays': 3.6.21(react@19.2.5)
+ '@react-stately/select': 3.9.0(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/combobox': 3.13.8(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/datepicker@3.15.1(react@19.2.4)':
+ '@react-stately/datepicker@3.15.1(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
'@internationalized/string': 3.2.7
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/overlays': 3.6.21(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/datepicker': 3.13.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/overlays': 3.6.21(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/datepicker': 3.13.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
'@react-stately/flags@3.1.2':
dependencies:
'@swc/helpers': 0.5.18
- '@react-stately/form@3.2.1(react@19.2.4)':
+ '@react-stately/form@3.2.1(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/form@3.2.2(react@19.2.4)':
+ '@react-stately/form@3.2.2(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/grid@3.11.7(react@19.2.4)':
+ '@react-stately/grid@3.11.7(react@19.2.5)':
dependencies:
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-types/grid': 3.3.6(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-types/grid': 3.3.6(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/list@3.13.0(react@19.2.4)':
+ '@react-stately/list@3.13.0(react@19.2.5)':
dependencies:
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/list@3.13.2(react@19.2.4)':
+ '@react-stately/list@3.13.2(react@19.2.5)':
dependencies:
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-stately/utils': 3.11.0(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-stately/utils': 3.11.0(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/menu@3.9.7(react@19.2.4)':
+ '@react-stately/menu@3.9.7(react@19.2.5)':
dependencies:
- '@react-stately/overlays': 3.6.21(react@19.2.4)
- '@react-types/menu': 3.10.4(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/overlays': 3.6.21(react@19.2.5)
+ '@react-types/menu': 3.10.4(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/menu@3.9.9(react@19.2.4)':
+ '@react-stately/menu@3.9.9(react@19.2.5)':
dependencies:
- '@react-stately/overlays': 3.6.21(react@19.2.4)
- '@react-types/menu': 3.10.5(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/overlays': 3.6.21(react@19.2.5)
+ '@react-types/menu': 3.10.5(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/numberfield@3.10.1(react@19.2.4)':
+ '@react-stately/numberfield@3.10.1(react@19.2.5)':
dependencies:
'@internationalized/number': 3.6.5
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/numberfield': 3.8.14(react@19.2.4)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/numberfield': 3.8.14(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/overlays@3.6.19(react@19.2.4)':
+ '@react-stately/overlays@3.6.19(react@19.2.5)':
dependencies:
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/overlays': 3.9.2(react@19.2.4)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/overlays': 3.9.2(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/overlays@3.6.21(react@19.2.4)':
+ '@react-stately/overlays@3.6.21(react@19.2.5)':
dependencies:
- '@react-stately/utils': 3.11.0(react@19.2.4)
- '@react-types/overlays': 3.9.2(react@19.2.4)
+ '@react-stately/utils': 3.11.0(react@19.2.5)
+ '@react-types/overlays': 3.9.2(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/radio@3.11.1(react@19.2.4)':
+ '@react-stately/radio@3.11.1(react@19.2.5)':
dependencies:
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/radio': 3.9.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/radio': 3.9.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/select@3.9.0(react@19.2.4)':
+ '@react-stately/select@3.9.0(react@19.2.5)':
dependencies:
- '@react-stately/form': 3.2.2(react@19.2.4)
- '@react-stately/list': 3.13.2(react@19.2.4)
- '@react-stately/overlays': 3.6.21(react@19.2.4)
- '@react-stately/utils': 3.11.0(react@19.2.4)
- '@react-types/select': 3.12.0(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/form': 3.2.2(react@19.2.5)
+ '@react-stately/list': 3.13.2(react@19.2.5)
+ '@react-stately/overlays': 3.6.21(react@19.2.5)
+ '@react-stately/utils': 3.11.0(react@19.2.5)
+ '@react-types/select': 3.12.0(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/selection@3.20.7(react@19.2.4)':
+ '@react-stately/selection@3.20.7(react@19.2.5)':
dependencies:
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/utils': 3.11.0(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/utils': 3.11.0(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/slider@3.7.1(react@19.2.4)':
+ '@react-stately/slider@3.7.1(react@19.2.5)':
dependencies:
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/slider': 3.8.2(react@19.2.4)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/slider': 3.8.2(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/table@3.15.0(react@19.2.4)':
+ '@react-stately/table@3.15.0(react@19.2.5)':
dependencies:
- '@react-stately/collections': 3.12.8(react@19.2.4)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
'@react-stately/flags': 3.1.2
- '@react-stately/grid': 3.11.7(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/grid': 3.3.5(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/table': 3.13.3(react@19.2.4)
+ '@react-stately/grid': 3.11.7(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/grid': 3.3.5(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/table': 3.13.3(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/tabs@3.8.5(react@19.2.4)':
+ '@react-stately/tabs@3.8.5(react@19.2.5)':
dependencies:
- '@react-stately/list': 3.13.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- '@react-types/tabs': 3.3.20(react@19.2.4)
+ '@react-stately/list': 3.13.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ '@react-types/tabs': 3.3.20(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/toast@3.1.2(react@19.2.4)':
+ '@react-stately/toast@3.1.2(react@19.2.5)':
dependencies:
'@swc/helpers': 0.5.18
- react: 19.2.4
- use-sync-external-store: 1.6.0(react@19.2.4)
+ react: 19.2.5
+ use-sync-external-store: 1.6.0(react@19.2.5)
- '@react-stately/toggle@3.9.1(react@19.2.4)':
+ '@react-stately/toggle@3.9.1(react@19.2.5)':
dependencies:
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/checkbox': 3.10.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/checkbox': 3.10.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/toggle@3.9.3(react@19.2.4)':
+ '@react-stately/toggle@3.9.3(react@19.2.5)':
dependencies:
- '@react-stately/utils': 3.11.0(react@19.2.4)
- '@react-types/checkbox': 3.10.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/utils': 3.11.0(react@19.2.5)
+ '@react-types/checkbox': 3.10.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/tooltip@3.5.7(react@19.2.4)':
+ '@react-stately/tooltip@3.5.7(react@19.2.5)':
dependencies:
- '@react-stately/overlays': 3.6.21(react@19.2.4)
- '@react-types/tooltip': 3.4.20(react@19.2.4)
+ '@react-stately/overlays': 3.6.21(react@19.2.5)
+ '@react-types/tooltip': 3.4.20(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/tree@3.9.2(react@19.2.4)':
+ '@react-stately/tree@3.9.2(react@19.2.5)':
dependencies:
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-stately/utils': 3.10.8(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-stately/utils': 3.10.8(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/tree@3.9.4(react@19.2.4)':
+ '@react-stately/tree@3.9.4(react@19.2.5)':
dependencies:
- '@react-stately/collections': 3.12.8(react@19.2.4)
- '@react-stately/selection': 3.20.7(react@19.2.4)
- '@react-stately/utils': 3.11.0(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-stately/collections': 3.12.8(react@19.2.5)
+ '@react-stately/selection': 3.20.7(react@19.2.5)
+ '@react-stately/utils': 3.11.0(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/utils@3.10.8(react@19.2.4)':
+ '@react-stately/utils@3.10.8(react@19.2.5)':
dependencies:
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/utils@3.11.0(react@19.2.4)':
+ '@react-stately/utils@3.11.0(react@19.2.5)':
dependencies:
'@swc/helpers': 0.5.18
- react: 19.2.4
+ react: 19.2.5
- '@react-stately/virtualizer@4.4.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@react-stately/virtualizer@4.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@react-aria/utils': 3.32.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
+ '@react-aria/utils': 3.32.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
'@swc/helpers': 0.5.18
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@react-types/accordion@3.0.0-alpha.26(react@19.2.4)':
+ '@react-types/accordion@3.0.0-alpha.26(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/breadcrumbs@3.7.16(react@19.2.4)':
+ '@react-types/breadcrumbs@3.7.16(react@19.2.5)':
dependencies:
- '@react-types/link': 3.6.5(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/link': 3.6.5(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/button@3.14.0(react@19.2.4)':
+ '@react-types/button@3.14.0(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/button@3.14.1(react@19.2.4)':
+ '@react-types/button@3.14.1(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/calendar@3.7.4(react@19.2.4)':
+ '@react-types/calendar@3.7.4(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/calendar@3.8.1(react@19.2.4)':
+ '@react-types/calendar@3.8.1(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/checkbox@3.10.1(react@19.2.4)':
+ '@react-types/checkbox@3.10.1(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/checkbox@3.10.2(react@19.2.4)':
+ '@react-types/checkbox@3.10.2(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/combobox@3.13.8(react@19.2.4)':
+ '@react-types/combobox@3.13.8(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/datepicker@3.13.1(react@19.2.4)':
+ '@react-types/datepicker@3.13.1(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
- '@react-types/calendar': 3.8.1(react@19.2.4)
- '@react-types/overlays': 3.9.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/calendar': 3.8.1(react@19.2.5)
+ '@react-types/overlays': 3.9.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/datepicker@3.13.2(react@19.2.4)':
+ '@react-types/datepicker@3.13.2(react@19.2.5)':
dependencies:
'@internationalized/date': 3.10.0
- '@react-types/calendar': 3.8.1(react@19.2.4)
- '@react-types/overlays': 3.9.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/calendar': 3.8.1(react@19.2.5)
+ '@react-types/overlays': 3.9.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/dialog@3.5.22(react@19.2.4)':
+ '@react-types/dialog@3.5.22(react@19.2.5)':
dependencies:
- '@react-types/overlays': 3.9.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/overlays': 3.9.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/form@3.7.15(react@19.2.4)':
+ '@react-types/form@3.7.15(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/grid@3.3.5(react@19.2.4)':
+ '@react-types/grid@3.3.5(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/grid@3.3.6(react@19.2.4)':
+ '@react-types/grid@3.3.6(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/link@3.6.4(react@19.2.4)':
+ '@react-types/link@3.6.4(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/link@3.6.5(react@19.2.4)':
+ '@react-types/link@3.6.5(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/listbox@3.7.4(react@19.2.4)':
+ '@react-types/listbox@3.7.4(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/menu@3.10.4(react@19.2.4)':
+ '@react-types/menu@3.10.4(react@19.2.5)':
dependencies:
- '@react-types/overlays': 3.9.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/overlays': 3.9.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/menu@3.10.5(react@19.2.4)':
+ '@react-types/menu@3.10.5(react@19.2.5)':
dependencies:
- '@react-types/overlays': 3.9.2(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/overlays': 3.9.2(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/numberfield@3.8.14(react@19.2.4)':
+ '@react-types/numberfield@3.8.14(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/overlays@3.9.1(react@19.2.4)':
+ '@react-types/overlays@3.9.1(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/overlays@3.9.2(react@19.2.4)':
+ '@react-types/overlays@3.9.2(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/progress@3.5.15(react@19.2.4)':
+ '@react-types/progress@3.5.15(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/radio@3.9.1(react@19.2.4)':
+ '@react-types/radio@3.9.1(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/select@3.12.0(react@19.2.4)':
+ '@react-types/select@3.12.0(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/shared@3.26.0(react@19.2.4)':
+ '@react-types/shared@3.26.0(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
- '@react-types/slider@3.8.2(react@19.2.4)':
+ '@react-types/slider@3.8.2(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/switch@3.5.15(react@19.2.4)':
+ '@react-types/switch@3.5.15(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/table@3.13.3(react@19.2.4)':
+ '@react-types/table@3.13.3(react@19.2.5)':
dependencies:
- '@react-types/grid': 3.3.5(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/grid': 3.3.5(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/tabs@3.3.20(react@19.2.4)':
+ '@react-types/tabs@3.3.20(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/textfield@3.12.5(react@19.2.4)':
+ '@react-types/textfield@3.12.5(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/textfield@3.12.6(react@19.2.4)':
+ '@react-types/textfield@3.12.6(react@19.2.5)':
dependencies:
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
- '@react-types/tooltip@3.4.20(react@19.2.4)':
+ '@react-types/tooltip@3.4.20(react@19.2.5)':
dependencies:
- '@react-types/overlays': 3.9.1(react@19.2.4)
- '@react-types/shared': 3.26.0(react@19.2.4)
- react: 19.2.4
+ '@react-types/overlays': 3.9.1(react@19.2.5)
+ '@react-types/shared': 3.26.0(react@19.2.5)
+ react: 19.2.5
'@rolldown/pluginutils@1.0.0-beta.53': {}
@@ -14877,7 +14885,7 @@ snapshots:
'@sentry/core@10.27.0': {}
- '@sentry/nextjs@10.27.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.104.1)':
+ '@sentry/nextjs@10.27.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.104.1)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.39.0
@@ -14887,10 +14895,10 @@ snapshots:
'@sentry/core': 10.27.0
'@sentry/node': 10.27.0
'@sentry/opentelemetry': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0)
- '@sentry/react': 10.27.0(react@19.2.4)
+ '@sentry/react': 10.27.0(react@19.2.5)
'@sentry/vercel-edge': 10.27.0
'@sentry/webpack-plugin': 4.7.0(webpack@5.104.1)
- next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
resolve: 1.22.8
rollup: 4.59.0
stacktrace-parser: 0.1.11
@@ -14968,12 +14976,12 @@ snapshots:
'@opentelemetry/semantic-conventions': 1.39.0
'@sentry/core': 10.27.0
- '@sentry/react@10.27.0(react@19.2.4)':
+ '@sentry/react@10.27.0(react@19.2.5)':
dependencies:
'@sentry/browser': 10.27.0
'@sentry/core': 10.27.0
hoist-non-react-statics: 3.3.2
- react: 19.2.4
+ react: 19.2.5
'@sentry/vercel-edge@10.27.0':
dependencies:
@@ -15501,17 +15509,17 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 4.1.18
- '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@tanstack/react-table@8.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@tanstack/table-core': 8.21.3
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- '@tanstack/react-virtual@3.11.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@tanstack/react-virtual@3.11.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@tanstack/virtual-core': 3.11.3
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
'@tanstack/table-core@8.21.3': {}
@@ -15537,12 +15545,12 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
- '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@babel/runtime': 7.28.6
'@testing-library/dom': 10.4.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
@@ -15908,7 +15916,7 @@ snapshots:
'@codemirror/state': 6.6.0
'@codemirror/view': 6.40.0
- '@uiw/react-codemirror@4.25.8(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@uiw/react-codemirror@4.25.8(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@babel/runtime': 7.28.6
'@codemirror/commands': 6.10.3
@@ -15917,8 +15925,8 @@ snapshots:
'@codemirror/view': 6.40.0
'@uiw/codemirror-extensions-basic-setup': 4.25.8(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.40.0)
codemirror: 6.0.2
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- '@codemirror/autocomplete'
- '@codemirror/language'
@@ -16230,10 +16238,10 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
- alert@6.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ alert@6.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
ansi-regex@5.0.1: {}
@@ -16379,6 +16387,8 @@ snapshots:
base64-js@1.5.1: {}
+ baseline-browser-mapping@2.10.19: {}
+
baseline-browser-mapping@2.9.15: {}
bidi-js@1.0.3:
@@ -16528,14 +16538,14 @@ snapshots:
clsx@2.1.1: {}
- cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
@@ -17152,13 +17162,13 @@ snapshots:
escape-string-regexp@5.0.0: {}
- eslint-config-next@16.1.6(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4):
+ eslint-config-next@16.2.3(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4):
dependencies:
- '@next/eslint-plugin-next': 16.1.6
+ '@next/eslint-plugin-next': 16.2.3
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
+ eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.0)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@@ -17184,7 +17194,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
+ eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -17195,18 +17205,18 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.0)(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4)
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
+ eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@@ -17216,7 +17226,7 @@ snapshots:
eslint-utils: 2.1.0
regexpp: 3.2.0
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.0)(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -17227,7 +17237,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.39.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -17609,14 +17619,14 @@ snapshots:
fraction.js@4.3.7: {}
- framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ framer-motion@11.18.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
motion-dom: 11.18.1
motion-utils: 11.18.1
tslib: 2.8.1
optionalDependencies:
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
fresh@2.0.0: {}
@@ -17975,10 +17985,10 @@ snapshots:
inline-style-parser@0.2.7: {}
- input-otp@1.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ input-otp@1.4.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
internal-slot@1.1.0:
dependencies:
@@ -18311,10 +18321,10 @@ snapshots:
kleur@4.1.5: {}
- langchain@1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11)):
+ langchain@1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.1(zod@4.1.11)):
dependencies:
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))
- '@langchain/langgraph': 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11)
+ '@langchain/langgraph': 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11)
'@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))
langsmith: 0.4.7(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))
uuid: 10.0.0
@@ -18450,13 +18460,13 @@ snapshots:
dependencies:
yallist: 3.1.1
- lucide-react@0.542.0(react@19.2.4):
+ lucide-react@0.542.0(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
- lucide-react@0.543.0(react@19.2.4):
+ lucide-react@0.543.0(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
lz-string@1.5.0: {}
@@ -19017,37 +19027,37 @@ snapshots:
neo-async@2.6.2: {}
- next-auth@5.0.0-beta.30(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4):
+ next-auth@5.0.0-beta.30(next@16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5):
dependencies:
'@auth/core': 0.41.0
- next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
+ next: 16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
- next-themes@0.2.1(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next-themes@0.2.1(next@16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
- next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ next: 16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next@16.2.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
- '@next/env': 16.1.6
+ '@next/env': 16.2.3
'@swc/helpers': 0.5.15
- baseline-browser-mapping: 2.9.15
+ baseline-browser-mapping: 2.10.19
caniuse-lite: 1.0.30001765
postcss: 8.4.31
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.5)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.1.6
- '@next/swc-darwin-x64': 16.1.6
- '@next/swc-linux-arm64-gnu': 16.1.6
- '@next/swc-linux-arm64-musl': 16.1.6
- '@next/swc-linux-x64-gnu': 16.1.6
- '@next/swc-linux-x64-musl': 16.1.6
- '@next/swc-win32-arm64-msvc': 16.1.6
- '@next/swc-win32-x64-msvc': 16.1.6
+ '@next/swc-darwin-arm64': 16.2.3
+ '@next/swc-darwin-x64': 16.2.3
+ '@next/swc-linux-arm64-gnu': 16.2.3
+ '@next/swc-linux-arm64-musl': 16.2.3
+ '@next/swc-linux-x64-gnu': 16.2.3
+ '@next/swc-linux-x64-musl': 16.2.3
+ '@next/swc-win32-arm64-msvc': 16.2.3
+ '@next/swc-win32-x64-msvc': 16.2.3
'@opentelemetry/api': 1.9.0
'@playwright/test': 1.56.1
babel-plugin-react-compiler: 1.0.0
@@ -19439,65 +19449,65 @@ snapshots:
queue-microtask@1.2.3: {}
- radix-ui@1.4.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ radix-ui@1.4.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-accordion': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-alert-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-checkbox': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-context-menu': 2.2.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-dropdown-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-form': 0.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-hover-card': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-menubar': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-navigation-menu': 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-one-time-password-field': 0.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-password-toggle-field': 0.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-popover': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-radio-group': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-scroll-area': 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-select': 2.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slider': 1.3.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-switch': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-tabs': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-toast': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-toggle': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-toggle-group': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-toolbar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-tooltip': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
- '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-accordion': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-alert-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-checkbox': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-context-menu': 2.2.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-dropdown-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-form': 0.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-hover-card': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-menubar': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-navigation-menu': 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-one-time-password-field': 0.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-password-toggle-field': 0.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-popover': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-radio-group': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-scroll-area': 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-select': 2.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slider': 1.3.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-switch': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-tabs': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-toast': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-toggle': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-toggle-group': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-toolbar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-tooltip': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.5)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
@@ -19511,21 +19521,21 @@ snapshots:
iconv-lite: 0.7.2
unpipe: 1.0.0
- react-day-picker@9.13.0(react@19.2.4):
+ react-day-picker@9.13.0(react@19.2.5):
dependencies:
'@date-fns/tz': 1.4.1
date-fns: 4.1.0
date-fns-jalali: 4.1.0-0
- react: 19.2.4
+ react: 19.2.5
- react-dom@19.2.4(react@19.2.4):
+ react-dom@19.2.5(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
scheduler: 0.27.0
- react-hook-form@7.62.0(react@19.2.4):
+ react-hook-form@7.62.0(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
react-is@16.13.1: {}
@@ -19533,7 +19543,7 @@ snapshots:
react-is@18.3.1: {}
- react-markdown@10.1.0(@types/react@19.2.8)(react@19.2.4):
+ react-markdown@10.1.0(@types/react@19.2.8)(react@19.2.5):
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
@@ -19542,7 +19552,7 @@ snapshots:
hast-util-to-jsx-runtime: 2.3.6
html-url-attributes: 3.0.1
mdast-util-to-hast: 13.2.1
- react: 19.2.4
+ react: 19.2.5
remark-parse: 11.0.0
remark-rehype: 11.1.2
unified: 11.0.5
@@ -19553,60 +19563,60 @@ snapshots:
react-refresh@0.18.0: {}
- react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.4):
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.5):
dependencies:
- react: 19.2.4
- react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.4)
+ react: 19.2.5
+ react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.5)
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.8
- react-remove-scroll@2.7.2(@types/react@19.2.8)(react@19.2.4):
+ react-remove-scroll@2.7.2(@types/react@19.2.8)(react@19.2.5):
dependencies:
- react: 19.2.4
- react-remove-scroll-bar: 2.3.8(@types/react@19.2.8)(react@19.2.4)
- react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.4)
+ react: 19.2.5
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.8)(react@19.2.5)
+ react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.5)
tslib: 2.8.1
- use-callback-ref: 1.3.3(@types/react@19.2.8)(react@19.2.4)
- use-sidecar: 1.1.3(@types/react@19.2.8)(react@19.2.4)
+ use-callback-ref: 1.3.3(@types/react@19.2.8)(react@19.2.5)
+ use-sidecar: 1.1.3(@types/react@19.2.8)(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
- react-smooth@4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ react-smooth@4.0.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
fast-equals: 5.4.0
prop-types: 15.8.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-transition-group: 4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- react-style-singleton@2.2.3(@types/react@19.2.8)(react@19.2.4):
+ react-style-singleton@2.2.3(@types/react@19.2.8)(react@19.2.5):
dependencies:
get-nonce: 1.0.1
- react: 19.2.4
+ react: 19.2.5
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.8
- react-textarea-autosize@8.5.9(@types/react@19.2.8)(react@19.2.4):
+ react-textarea-autosize@8.5.9(@types/react@19.2.8)(react@19.2.5):
dependencies:
'@babel/runtime': 7.28.6
- react: 19.2.4
- use-composed-ref: 1.4.0(@types/react@19.2.8)(react@19.2.4)
- use-latest: 1.3.0(@types/react@19.2.8)(react@19.2.4)
+ react: 19.2.5
+ use-composed-ref: 1.4.0(@types/react@19.2.8)(react@19.2.5)
+ use-latest: 1.3.0(@types/react@19.2.8)(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ react-transition-group@4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@babel/runtime': 7.28.6
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- react@19.2.4: {}
+ react@19.2.5: {}
readdirp@3.6.0:
dependencies:
@@ -19624,15 +19634,15 @@ snapshots:
dependencies:
decimal.js-light: 2.5.1
- recharts@2.15.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ recharts@2.15.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
clsx: 2.1.1
eventemitter3: 4.0.7
lodash: 4.17.23
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
react-is: 18.3.1
- react-smooth: 4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react-smooth: 4.0.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
recharts-scale: 0.4.5
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
@@ -20158,17 +20168,17 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
- streamdown@1.6.10(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.4):
+ streamdown@1.6.10(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.5):
dependencies:
clsx: 2.1.1
hast: 1.0.0
hast-util-to-jsx-runtime: 2.3.6
html-url-attributes: 3.0.1
katex: 0.16.27
- lucide-react: 0.542.0(react@19.2.4)
+ lucide-react: 0.542.0(react@19.2.5)
marked: 16.4.2
mermaid: 11.12.2
- react: 19.2.4
+ react: 19.2.5
rehype-harden: 1.1.7
rehype-katex: 7.0.1
rehype-raw: 7.0.0
@@ -20302,10 +20312,10 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
- styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.4):
+ styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.5):
dependencies:
client-only: 0.0.1
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@babel/core': 7.28.6
@@ -20321,11 +20331,11 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
- swr@2.3.8(react@19.2.4):
+ swr@2.3.8(react@19.2.5):
dependencies:
dequal: 2.0.3
- react: 19.2.4
- use-sync-external-store: 1.6.0(react@19.2.4)
+ react: 19.2.5
+ use-sync-external-store: 1.6.0(react@19.2.5)
symbol-tree@3.2.4: {}
@@ -20617,47 +20627,47 @@ snapshots:
dependencies:
punycode: 2.3.1
- use-callback-ref@1.3.3(@types/react@19.2.8)(react@19.2.4):
+ use-callback-ref@1.3.3(@types/react@19.2.8)(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.8
- use-composed-ref@1.4.0(@types/react@19.2.8)(react@19.2.4):
+ use-composed-ref@1.4.0(@types/react@19.2.8)(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- use-isomorphic-layout-effect@1.2.1(@types/react@19.2.8)(react@19.2.4):
+ use-isomorphic-layout-effect@1.2.1(@types/react@19.2.8)(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.8
- use-latest@1.3.0(@types/react@19.2.8)(react@19.2.4):
+ use-latest@1.3.0(@types/react@19.2.8)(react@19.2.5):
dependencies:
- react: 19.2.4
- use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.8)(react@19.2.4)
+ react: 19.2.5
+ use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.8)(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.8
- use-sidecar@1.1.3(@types/react@19.2.8)(react@19.2.4):
+ use-sidecar@1.1.3(@types/react@19.2.8)(react@19.2.5):
dependencies:
detect-node-es: 1.1.0
- react: 19.2.4
+ react: 19.2.5
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.8
- use-stick-to-bottom@1.1.1(react@19.2.4):
+ use-stick-to-bottom@1.1.1(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
- use-sync-external-store@1.6.0(react@19.2.4):
+ use-sync-external-store@1.6.0(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
util-deprecate@1.0.2: {}
@@ -20673,11 +20683,11 @@ snapshots:
vary@1.1.2: {}
- vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
- '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
@@ -20988,10 +20998,10 @@ snapshots:
zod@4.1.11: {}
- zustand@5.0.8(@types/react@19.2.8)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
+ zustand@5.0.8(@types/react@19.2.8)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)):
optionalDependencies:
'@types/react': 19.2.8
- react: 19.2.4
- use-sync-external-store: 1.6.0(react@19.2.4)
+ react: 19.2.5
+ use-sync-external-store: 1.6.0(react@19.2.5)
zwitch@2.0.4: {}
diff --git a/ui/proxy.ts b/ui/proxy.ts
index 553de6e9ba..d3bb8eb20e 100644
--- a/ui/proxy.ts
+++ b/ui/proxy.ts
@@ -1,7 +1,7 @@
-import { NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
+import type { NextAuthRequest } from "next-auth";
import { auth } from "@/auth.config";
-import { INVITATION_ACTION_PARAM } from "@/lib/invitation-routing";
const publicRoutes = [
"/sign-in",
@@ -18,24 +18,9 @@ const isPublicRoute = (pathname: string): boolean => {
};
// NextAuth's auth() wrapper - renamed from middleware to proxy
-export default auth((req: NextRequest & { auth: any }) => {
+export default auth((req: NextAuthRequest) => {
const { pathname } = req.nextUrl;
- // Backward compatibility: redirect old invitation links to new smart router
- // Skip redirect when the user explicitly chose "Create an account" from the smart router
- if (
- pathname === "/sign-up" &&
- req.nextUrl.searchParams.has("invitation_token") &&
- !req.nextUrl.searchParams.has(INVITATION_ACTION_PARAM)
- ) {
- const acceptUrl = new URL("/invitation/accept", req.url);
- acceptUrl.searchParams.set(
- "invitation_token",
- req.nextUrl.searchParams.get("invitation_token")!,
- );
- return NextResponse.redirect(acceptUrl);
- }
-
const user = req.auth?.user;
const sessionError = req.auth?.error;
diff --git a/ui/tests/auth/auth-middleware.spec.ts b/ui/tests/auth/auth-middleware.spec.ts
index d7f1d23619..b56a2b63ca 100644
--- a/ui/tests/auth/auth-middleware.spec.ts
+++ b/ui/tests/auth/auth-middleware.spec.ts
@@ -76,4 +76,28 @@ test.describe("Middleware Error Handling", () => {
// Note: Billing and integrations permission tests removed
// These features only exist in Prowler Cloud, not in the open-source version
+
+ test(
+ "should not redirect /sign-up?invitation_token=... to /invitation/accept",
+ { tag: ["@e2e", "@auth", "@middleware", "@AUTH-MW-E2E-003"] },
+ async ({ page, context }) => {
+ const signUpPage = new SignUpPage(page);
+ await context.clearCookies();
+
+ const token = "test-token-regression";
+ const response = await page.goto(
+ `/sign-up?invitation_token=${token}`,
+ { waitUntil: "commit" },
+ );
+
+ // The middleware must not rewrite the URL any more. Assert the final
+ // URL stayed on /sign-up with the token intact, and that the sign-up
+ // form actually rendered (guards against "URL stayed but page broke").
+ expect(response?.status()).toBe(200);
+ await expect(page).toHaveURL(
+ `/sign-up?invitation_token=${token}`,
+ );
+ await signUpPage.verifyPageLoaded();
+ },
+ );
});
diff --git a/ui/tests/sign-up/sign-up-page.ts b/ui/tests/sign-up/sign-up-page.ts
index e3d3cc3e72..0afaccb6bf 100644
--- a/ui/tests/sign-up/sign-up-page.ts
+++ b/ui/tests/sign-up/sign-up-page.ts
@@ -54,7 +54,6 @@ export class SignUpPage extends BasePage {
}
async verifyPageLoaded(): Promise {
- await expect(this.page).toHaveURL("/sign-up");
await expect(this.emailInput).toBeVisible();
await expect(this.submitButton).toBeVisible();
}
diff --git a/ui/types/components.ts b/ui/types/components.ts
index 0f294ceb7d..62642db4ea 100644
--- a/ui/types/components.ts
+++ b/ui/types/components.ts
@@ -82,6 +82,7 @@ export type PermissionState =
export const FINDING_DELTA = {
NEW: "new",
CHANGED: "changed",
+ NONE: "none",
} as const;
export type FindingDelta =
| (typeof FINDING_DELTA)[keyof typeof FINDING_DELTA]
diff --git a/ui/types/filters.ts b/ui/types/filters.ts
index 40e1c7b7a0..9630318712 100644
--- a/ui/types/filters.ts
+++ b/ui/types/filters.ts
@@ -15,6 +15,7 @@ export interface FilterOption {
key: string;
labelCheckboxGroup: string;
values: string[];
+ width?: "default" | "wide";
valueLabelMapping?: Array<{ [uid: string]: FilterEntity }>;
labelFormatter?: (value: string) => string;
index?: number;
@@ -38,7 +39,9 @@ export enum FilterType {
RESOURCE_TYPE = "resource_type__in",
SEVERITY = "severity__in",
STATUS = "status__in",
- DELTA = "delta__in",
+ // The API only registers `delta` (exact, singular). `delta__in` is silently
+ // dropped, so the dropdown, URL, and backend must all use `delta`.
+ DELTA = "delta",
CATEGORY = "category__in",
RESOURCE_GROUPS = "resource_groups__in",
}
@@ -67,11 +70,13 @@ export type FilterParam =
| "filter[severity__in]"
| "filter[status__in]"
| "filter[delta__in]"
+ | "filter[delta]"
| "filter[region__in]"
| "filter[service__in]"
| "filter[resource_type__in]"
| "filter[category__in]"
| "filter[resource_groups__in]"
+ | "filter[scan]"
| "filter[scan__in]"
| "filter[scan_id]"
| "filter[scan_id__in]"