Compare commits

...
10 Commits
Author SHA1 Message Date
b313a6dde1 fix(api): recover missing scan resources (#12089)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-07-22 13:10:36 +01:00
Prowler BotandPedro Martín 0772169b5c fix(ci): ignore unfixed Perl Storable CVE-2026-57433 (#12090)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2026-07-22 13:44:55 +02:00
1f27066635 fix(ui): hide billing when Cloud billing is disabled (#12070)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: César Arroba <cesar@prowler.com>
2026-07-22 10:55:28 +02:00
César Arroba f815380c3f fix(ui): resolve leftover merge conflict from vitest backport (#12078) (#12080) 2026-07-22 10:36:19 +02:00
Prowler BotandCésar Arroba 5b8dab973e fix(ui): bump vitest to 4.1.10 to resolve @vitest/browser file-access bypass (#12078)
Co-authored-by: César Arroba <19954079+cesararroba@users.noreply.github.com>
2026-07-22 10:09:15 +02:00
14e50ede21 fix(ui): adjust sidebar logo top spacing (#12072)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-07-21 17:05:27 +01:00
Prowler BotandCésar Arroba 11317c4ec0 fix(ui): remove unused npm from container to drop tar CVE-2026-59873 (#12073)
Co-authored-by: César Arroba <19954079+cesararroba@users.noreply.github.com>
2026-07-21 16:39:25 +01:00
ab325c9099 fix(alibabacloud): normalize security group policy case (#12059)
Co-authored-by: Xianyao Chen <owenchenxy@gmail.com>
Co-authored-by: xianyao.chen <xychen@xianyaochens-MacBook-Pro.local>
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-07-20 14:23:35 +01:00
Prowler Botandprowler-bot 6fe8646903 chore(release): Bump versions to v5.35.1 (#12043)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-07-17 13:08:43 +02:00
Prowler Botandprowler-bot 9b0c4a0073 chore(api): Update prowler dependency to v5.35 for release 5.35.0 (#12039)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-07-17 12:10:01 +02:00
28 changed files with 960 additions and 162 deletions
+1 -1
View File
@@ -158,7 +158,7 @@ SENTRY_RELEASE=local
# REO_DEV_CLIENT_ID=
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.35.0
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.35.1
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
+14
View File
@@ -35,6 +35,20 @@ CVE-2026-13221 pkg:perl-base exp:2026-08-15
CVE-2026-13221 pkg:perl-modules-5.36 exp:2026-08-15
CVE-2026-13221 pkg:libperl5.36 exp:2026-08-15
# CVE-2026-57433 — Perl Storable signed integer overflow when deserializing a
# crafted SX_HOOK record (retrieve_hook_common passes a wrapped negative count
# to av_extend).
# Packages: perl, perl-base, perl-modules-5.36, libperl5.36.
# Why ignored: perl-base is part of Debian's "Essential: yes" set; it cannot be
# removed without breaking dpkg. Prowler does not invoke perl at runtime and
# never calls Storable's thaw/retrieve on attacker-controlled blobs, so the
# vulnerable deserialization path is unreachable. Fixed upstream in
# Storable 3.41; no Debian bookworm fix is available yet.
CVE-2026-57433 pkg:perl exp:2026-08-15
CVE-2026-57433 pkg:perl-base exp:2026-08-15
CVE-2026-57433 pkg:perl-modules-5.36 exp:2026-08-15
CVE-2026-57433 pkg:libperl5.36 exp:2026-08-15
# CVE-2025-7458 — SQLite integer overflow.
# Package: libsqlite3-0.
# Why ignored: transitive dependency of CPython's stdlib sqlite3 module. The
@@ -0,0 +1 @@
Scan findings now recover resources missing from the in-memory cache after resource pre-resolution, preventing valid findings from being skipped
+2 -2
View File
@@ -45,7 +45,7 @@ dependencies = [
"gunicorn==26.0.0",
"uvloop==0.22.1",
"lxml==6.1.0",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.35",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (==1.3.0)",
"sentry-sdk[django] (==2.56.0)",
@@ -71,7 +71,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.36.0"
version = "1.36.1"
# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml).
# target-version tracks this project's lowest supported Python.
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.36.0
version: 1.36.1
description: |-
Prowler API specification.
+150 -20
View File
@@ -1,3 +1,4 @@
import copy
import csv
import io
import json
@@ -605,6 +606,45 @@ def _process_finding_micro_batch(
scan_resource_groups_cache: Dict tracking resource group counts {(resource_group, severity): {"total", "failed", "new_failed"}}.
group_resources_cache: Dict tracking unique resources per group {resource_group: set(resource_uids)}.
"""
def build_resource_defaults_from_finding(finding: ProwlerFinding) -> dict[str, Any]:
check_metadata = finding.get_metadata()
group = check_metadata.get("resourcegroup") or None
return {
"tenant_id": tenant_id,
"provider": provider_instance,
"uid": finding.resource_uid,
"region": finding.region,
"service": finding.service_name,
"type": finding.resource_type,
"name": finding.resource_name,
"groups": [group] if group else None,
}
def recover_resource_after_cache_miss(finding: ProwlerFinding) -> Resource:
resource_uid = finding.resource_uid
resource_instance = Resource.objects.filter(
tenant_id=tenant_id,
provider_id=provider_instance.id,
uid=resource_uid,
).first()
if resource_instance is None:
try:
with transaction.atomic():
resource_instance = Resource.objects.create(
**build_resource_defaults_from_finding(finding)
)
except IntegrityError:
resource_instance = Resource.objects.filter(
tenant_id=tenant_id,
provider_id=provider_instance.id,
uid=resource_uid,
).first()
if resource_instance is None:
raise
return cache_resource(resource_uid, resource_instance)
# Accumulate objects for bulk operations
findings_to_create = []
dirty_resources = {}
@@ -643,7 +683,103 @@ def _process_finding_micro_batch(
# All DB writes for this micro-batch run inside ONE rls_transaction,
# with deadlock-retry at micro-batch granularity instead of per-finding.
missing_cache_value = object()
for attempt in range(CELERY_DEADLOCK_ATTEMPTS):
resource_cache_originals: dict[str, Resource | object] = {}
failed_count_originals: dict[str, int | None] = {}
resource_field_originals: dict[str, dict[str, Any]] = {}
tag_cache_original = dict(tag_cache)
scan_resource_cache_original = set(scan_resource_cache)
scan_categories_cache_original = {
key: value.copy() for key, value in scan_categories_cache.items()
}
scan_resource_groups_cache_original = {
key: value.copy() for key, value in scan_resource_groups_cache.items()
}
group_resources_cache_original = {
key: set(value) for key, value in group_resources_cache.items()
}
def cache_resource(resource_uid: str, resource_instance: Resource) -> Resource:
if resource_uid not in resource_cache_originals:
resource_cache_originals[resource_uid] = resource_cache.get(
resource_uid, missing_cache_value
)
resource_cache[resource_uid] = resource_instance
if resource_uid not in resource_failed_findings_cache:
failed_count_originals[resource_uid] = None
resource_failed_findings_cache[resource_uid] = 0
return resource_instance
def snapshot_failed_count(resource_uid: str) -> None:
if resource_uid not in failed_count_originals:
failed_count_originals[resource_uid] = (
resource_failed_findings_cache.get(resource_uid)
)
def snapshot_resource_fields(
resource_uid: str, resource_instance: Resource
) -> None:
if resource_uid in resource_field_originals:
return
resource_field_originals[resource_uid] = {
field: copy.deepcopy(getattr(resource_instance, field))
for field in (
"name",
"metadata",
"details",
"partition",
"region",
"service",
"type",
"groups",
"updated_at",
)
}
def restore_attempt_caches() -> None:
for resource_uid, original_fields in resource_field_originals.items():
resource_instance = resource_cache.get(resource_uid)
if resource_instance is None:
continue
for field, value in original_fields.items():
setattr(resource_instance, field, value)
for resource_uid, original_resource in resource_cache_originals.items():
if original_resource is missing_cache_value:
resource_cache.pop(resource_uid, None)
else:
resource_cache[resource_uid] = original_resource
for resource_uid, original_count in failed_count_originals.items():
if original_count is None:
resource_failed_findings_cache.pop(resource_uid, None)
else:
resource_failed_findings_cache[resource_uid] = original_count
tag_cache.clear()
tag_cache.update(tag_cache_original)
scan_resource_cache.clear()
scan_resource_cache.update(scan_resource_cache_original)
scan_categories_cache.clear()
scan_categories_cache.update(
{
key: value.copy()
for key, value in scan_categories_cache_original.items()
}
)
scan_resource_groups_cache.clear()
scan_resource_groups_cache.update(
{
key: value.copy()
for key, value in scan_resource_groups_cache_original.items()
}
)
group_resources_cache.clear()
group_resources_cache.update(
{
key: set(value)
for key, value in group_resources_cache_original.items()
}
)
try:
with rls_transaction(tenant_id):
# 1) Pre-resolve Resources in bulk
@@ -678,19 +814,8 @@ def _process_finding_micro_batch(
resources_to_create = []
for uid in missing_uids:
f = first_finding_per_uid[uid]
check_metadata = f.get_metadata()
group = check_metadata.get("resourcegroup") or None
resources_to_create.append(
Resource(
tenant_id=tenant_id,
provider=provider_instance,
uid=uid,
region=f.region,
service=f.service_name,
type=f.resource_type,
name=f.resource_name,
groups=[group] if group else None,
)
Resource(**build_resource_defaults_from_finding(f))
)
Resource.objects.bulk_create(
resources_to_create,
@@ -711,8 +836,7 @@ def _process_finding_micro_batch(
}
)
for uid, r in existing_resources.items():
resource_cache[uid] = r
resource_failed_findings_cache.setdefault(uid, 0)
cache_resource(uid, r)
# 2) Pre-resolve ResourceTags in bulk
batch_tag_kv: set[tuple[str, str]] = set()
@@ -758,47 +882,50 @@ def _process_finding_micro_batch(
resource_uid = finding.resource_uid
resource_instance = resource_cache.get(resource_uid)
if resource_instance is None:
# Should be unreachable after the pre-resolve step. Defensive log.
logger.error(
f"Resource {resource_uid} missing from cache after pre-resolve "
f"on scan {scan_instance.id}; skipping finding."
)
continue
resource_instance = recover_resource_after_cache_miss(finding)
# Detect resource field changes (defer save until end-of-batch bulk_update).
check_metadata = finding.get_metadata()
group = check_metadata.get("resourcegroup") or None
updated = False
if finding.region and resource_instance.region != finding.region:
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.region = finding.region
updated = True
if (
finding.resource_name
and resource_instance.name != finding.resource_name
):
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.name = finding.resource_name
updated = True
if resource_instance.service != finding.service_name:
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.service = finding.service_name
updated = True
if resource_instance.type != finding.resource_type:
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.type = finding.resource_type
updated = True
if resource_instance.metadata != finding.resource_metadata:
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.metadata = json.dumps(
finding.resource_metadata, cls=CustomEncoder
)
updated = True
if resource_instance.details != finding.resource_details:
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.details = finding.resource_details
updated = True
if resource_instance.partition != finding.partition:
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.partition = finding.partition
updated = True
if group and (
not resource_instance.groups
or group not in resource_instance.groups
):
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.groups = (resource_instance.groups or []) + [
group
]
@@ -860,6 +987,7 @@ def _process_finding_micro_batch(
muted_reason = mute_rules_cache[finding_uid]
if status == FindingStatus.FAIL and not is_muted:
snapshot_failed_count(resource_uid)
resource_failed_findings_cache[resource_uid] += 1
check_metadata["compliance"] = finding.compliance
@@ -1013,6 +1141,7 @@ def _process_finding_micro_batch(
if r is None:
continue
# Manually bump updated_at since bulk_update bypasses auto_now.
snapshot_resource_fields(uid, r)
r.updated_at = now_utc
resources_to_bulk_update.append(r)
if resources_to_bulk_update:
@@ -1034,6 +1163,7 @@ def _process_finding_micro_batch(
# Successful execution: leave deadlock retry loop.
break
except (OperationalError, IntegrityError) as db_err:
restore_attempt_caches()
if attempt < CELERY_DEADLOCK_ATTEMPTS - 1:
logger.warning(
f"{'Deadlock error' if isinstance(db_err, OperationalError) else 'Integrity error'} "
+646 -22
View File
@@ -2,6 +2,7 @@ import csv
import json
import re
import uuid
from collections.abc import MutableMapping
from contextlib import contextmanager
from datetime import UTC, datetime
from io import StringIO
@@ -15,13 +16,16 @@ from api.models import (
MuteRule,
Provider,
Resource,
ResourceFindingMapping,
ResourceScanSummary,
ResourceTag,
ResourceTagMapping,
Scan,
ScanSummary,
StateChoices,
StatusChoices,
)
from django.db import IntegrityError, OperationalError
from django.db import IntegrityError, OperationalError, transaction
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
from tasks.jobs.scan import (
@@ -52,6 +56,12 @@ def noop_rls_transaction(*args, **kwargs):
yield
@contextmanager
def atomic_rls_transaction(*args, **kwargs):
with transaction.atomic():
yield
class FakeFinding:
def __init__(self, **attrs):
self.metadata = attrs.pop("metadata", {})
@@ -70,6 +80,32 @@ class FakeFinding:
return self.metadata
class CacheMissAfterPreResolve(MutableMapping):
def __init__(self, missing_uid):
self._cache = {}
self.missing_uid = missing_uid
def __contains__(self, key):
if key == self.missing_uid:
return True
return key in self._cache
def __getitem__(self, key):
return self._cache[key]
def __setitem__(self, key, value):
self._cache[key] = value
def __delitem__(self, key):
del self._cache[key]
def __iter__(self):
return iter(self._cache)
def __len__(self):
return len(self._cache)
@pytest.mark.django_db
class TestPerformScan:
def test_perform_prowler_scan_success(
@@ -1054,8 +1090,12 @@ class TestPerformScan:
perform_prowler_scan(tenant_id, scan_id, provider_id, [])
# Verify findings are muted with correct reason
fail_finding_db = Finding.objects.get(uid=finding_uid_1)
pass_finding_db = Finding.objects.get(uid=finding_uid_2)
fail_finding_db = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding_uid_1
)
pass_finding_db = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding_uid_2
)
assert fail_finding_db.muted
assert fail_finding_db.muted_reason == mute_rule_reason
@@ -1066,7 +1106,9 @@ class TestPerformScan:
assert pass_finding_db.muted_at is not None
# Verify failed_findings_count is 0 for muted FAIL finding
resource_1 = Resource.objects.get(uid="resource_uid_1")
resource_1 = Resource.objects.get(
tenant_id=tenant.id, provider_id=provider.id, uid="resource_uid_1"
)
assert resource_1.failed_findings_count == 0
def test_perform_prowler_scan_with_inactive_mute_rules(
@@ -1146,13 +1188,17 @@ class TestPerformScan:
perform_prowler_scan(tenant_id, scan_id, provider_id, [])
# Verify finding is NOT muted
finding_db = Finding.objects.get(uid=finding_uid)
finding_db = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding_uid
)
assert not finding_db.muted
assert finding_db.muted_reason is None
assert finding_db.muted_at is None
# Verify failed_findings_count increments for FAIL finding
resource = Resource.objects.get(uid="resource_uid_inactive")
resource = Resource.objects.get(
tenant_id=tenant.id, provider_id=provider.id, uid="resource_uid_inactive"
)
assert resource.failed_findings_count == 1
def test_perform_prowler_scan_mutelist_overrides_mute_rules(
@@ -1232,13 +1278,17 @@ class TestPerformScan:
perform_prowler_scan(tenant_id, scan_id, provider_id, [])
# Verify mutelist reason takes precedence
finding_db = Finding.objects.get(uid=finding_uid)
finding_db = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding_uid
)
assert finding_db.muted
assert finding_db.muted_reason == "Muted by mutelist"
assert finding_db.muted_at is not None
# Verify failed_findings_count is 0
resource = Resource.objects.get(uid="resource_both")
resource = Resource.objects.get(
tenant_id=tenant.id, provider_id=provider.id, uid="resource_both"
)
assert resource.failed_findings_count == 0
def test_perform_prowler_scan_mute_rules_multiple_findings(
@@ -1330,14 +1380,20 @@ class TestPerformScan:
# Verify all findings are muted with same reason
for uid in finding_uids:
finding_db = Finding.objects.get(uid=uid)
finding_db = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=uid
)
assert finding_db.muted
assert finding_db.muted_reason == mute_rule_reason
assert finding_db.muted_at is not None
# Verify all resources have failed_findings_count = 0
for i in range(len(finding_uids)):
resource = Resource.objects.get(uid=f"resource_bulk_{i}")
resource = Resource.objects.get(
tenant_id=tenant.id,
provider_id=provider.id,
uid=f"resource_bulk_{i}",
)
assert resource.failed_findings_count == 0
def test_perform_prowler_scan_mute_rules_error_handling(
@@ -1415,12 +1471,18 @@ class TestPerformScan:
assert scan.state == StateChoices.COMPLETED
# Verify finding is not muted (mute_rules_cache was empty dict)
finding_db = Finding.objects.get(uid="finding_error_handling")
finding_db = Finding.objects.get(
tenant_id=tenant.id,
scan_id=scan.id,
uid="finding_error_handling",
)
assert not finding_db.muted
assert finding_db.muted_reason is None
# Verify failed_findings_count increments
resource = Resource.objects.get(uid="resource_error")
resource = Resource.objects.get(
tenant_id=tenant.id, provider_id=provider.id, uid="resource_error"
)
assert resource.failed_findings_count == 1
def test_perform_prowler_scan_muted_at_timestamp(
@@ -1502,7 +1564,9 @@ class TestPerformScan:
after_scan = datetime.now(UTC)
# Verify muted_at is within the scan time window
finding_db = Finding.objects.get(uid=finding_uid)
finding_db = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding_uid
)
assert finding_db.muted
assert finding_db.muted_at is not None
assert before_scan <= finding_db.muted_at <= after_scan
@@ -1513,6 +1577,548 @@ class TestPerformScan:
@pytest.mark.django_db
class TestProcessFindingMicroBatch:
def _process_one_finding_micro_batch(
self,
tenant,
scan,
provider,
finding,
resource_cache=None,
resource_failed_findings_cache=None,
):
resource_cache = resource_cache if resource_cache is not None else {}
resource_failed_findings_cache = (
resource_failed_findings_cache
if resource_failed_findings_cache is not None
else {}
)
caches = {
"resource_cache": resource_cache,
"tag_cache": {},
"last_status_cache": {},
"resource_failed_findings_cache": resource_failed_findings_cache,
"unique_resources": set(),
"scan_resource_cache": set(),
"mute_rules_cache": {},
"scan_categories_cache": {},
"scan_resource_groups_cache": {},
"group_resources_cache": {},
}
with (
patch("tasks.jobs.scan.rls_transaction", new=noop_rls_transaction),
patch("api.db_utils.rls_transaction", new=noop_rls_transaction),
):
_process_finding_micro_batch(
str(tenant.id),
[finding],
scan,
provider,
caches["resource_cache"],
caches["tag_cache"],
caches["last_status_cache"],
caches["resource_failed_findings_cache"],
caches["unique_resources"],
caches["scan_resource_cache"],
caches["mute_rules_cache"],
caches["scan_categories_cache"],
caches["scan_resource_groups_cache"],
caches["group_resources_cache"],
)
return caches
def test_process_finding_micro_batch_fallback_creates_resource_after_cache_miss(
self, tenants_fixture, scans_fixture
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = scan.provider
resource_uid = "arn:aws:accessanalyzer:us-east-1:123456789012:analyzer/unknown"
finding = FakeFinding(
uid="finding-cache-miss-create",
status=StatusChoices.FAIL,
status_extended="missing analyzer",
severity=Severity.medium,
check_id="accessanalyzer_enabled",
resource_uid=resource_uid,
resource_name="analyzer/unknown",
region="us-east-1",
service_name="accessanalyzer",
resource_type="analyzer",
resource_tags={},
resource_metadata={},
resource_details={},
partition="aws",
raw={},
compliance={},
metadata={"resourcegroup": "identity"},
muted=False,
)
caches = self._process_one_finding_micro_batch(
tenant,
scan,
provider,
finding,
resource_cache=CacheMissAfterPreResolve(resource_uid),
)
resource = Resource.objects.get(
tenant_id=tenant.id, provider_id=provider.id, uid=resource_uid
)
created_finding = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
)
assert created_finding.scan_id == scan.id
assert resource.provider_id == provider.id
assert resource.region == finding.region
assert resource.service == finding.service_name
assert resource.type == finding.resource_type
assert resource.name == finding.resource_name
assert resource.groups == ["identity"]
assert resource.findings.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
).exists()
assert caches["resource_cache"][resource_uid].id == resource.id
assert caches["resource_failed_findings_cache"][resource_uid] == 1
def test_process_finding_micro_batch_fallback_recovers_existing_resource_after_cache_miss(
self, tenants_fixture, scans_fixture
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = scan.provider
resource_uid = "arn:aws:guardduty:us-east-1:123456789012:detector/unknown"
existing_resource = Resource.objects.create(
tenant_id=tenant.id,
provider=provider,
uid=resource_uid,
name="detector/unknown",
region="us-east-1",
service="guardduty",
type="detector",
)
finding = FakeFinding(
uid="finding-cache-miss-existing",
status=StatusChoices.FAIL,
status_extended="missing detector",
severity=Severity.high,
check_id="guardduty_enabled",
resource_uid=resource_uid,
resource_name=existing_resource.name,
region=existing_resource.region,
service_name=existing_resource.service,
resource_type=existing_resource.type,
resource_tags={},
resource_metadata={},
resource_details={},
partition="aws",
raw={},
compliance={},
metadata={},
muted=False,
)
caches = self._process_one_finding_micro_batch(
tenant,
scan,
provider,
finding,
resource_cache=CacheMissAfterPreResolve(resource_uid),
)
assert (
Resource.objects.filter(
tenant_id=tenant.id, provider_id=provider.id, uid=resource_uid
).count()
== 1
)
created_finding = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
)
existing_resource.refresh_from_db()
assert created_finding.scan_id == scan.id
assert existing_resource.findings.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
).exists()
assert caches["resource_cache"][resource_uid].id == existing_resource.id
assert caches["resource_failed_findings_cache"][resource_uid] == 1
def test_process_finding_micro_batch_fallback_recovers_after_create_race(
self, tenants_fixture, scans_fixture
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = scan.provider
resource_uid = "arn:aws:securityhub:us-east-1:123456789012:hub/unknown"
raced_resource = Resource.objects.create(
tenant_id=tenant.id,
provider=provider,
uid=resource_uid,
name="hub/unknown",
region="us-east-1",
service="securityhub",
type="hub",
)
finding = FakeFinding(
uid="finding-cache-miss-failure",
status=StatusChoices.FAIL,
status_extended="missing hub",
severity=Severity.high,
check_id="securityhub_enabled",
resource_uid=resource_uid,
resource_name="hub/unknown",
region="us-east-1",
service_name="securityhub",
resource_type="hub",
resource_tags={},
resource_metadata={},
resource_details={},
partition="aws",
raw={},
compliance={},
metadata={},
muted=False,
)
resource_filter_result = MagicMock()
resource_filter_result.first.side_effect = [None, raced_resource]
with (
patch.object(
Resource.objects,
"filter",
return_value=resource_filter_result,
),
patch.object(
Resource.objects,
"create",
side_effect=IntegrityError("duplicate resource"),
),
):
caches = self._process_one_finding_micro_batch(
tenant,
scan,
provider,
finding,
resource_cache=CacheMissAfterPreResolve(resource_uid),
)
assert (
Resource.objects.filter(
tenant_id=tenant.id, provider_id=provider.id, uid=resource_uid
).count()
== 1
)
created_finding = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
)
raced_resource.refresh_from_db()
assert created_finding.scan_id == scan.id
assert raced_resource.findings.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
).exists()
assert caches["resource_cache"][resource_uid].id == raced_resource.id
assert caches["resource_failed_findings_cache"][resource_uid] == 1
def test_process_finding_micro_batch_cache_miss_retry_drops_rolled_back_resource(
self, tenants_fixture, scans_fixture
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = scan.provider
resource_uid = "generic-resource-cache-miss-retry"
cached_resource = Resource.objects.create(
tenant_id=tenant.id,
provider=provider,
uid="generic-cached-resource-retry",
name="old-cached-resource",
region="us-west-2",
service="old-service",
type="old-type",
)
finding = FakeFinding(
uid="finding-cache-miss-retry-clean-resource-cache",
status=StatusChoices.FAIL,
status_extended="missing resource",
severity=Severity.high,
check_id="generic_resource_check",
resource_uid=resource_uid,
resource_name="generic-resource",
region="us-east-1",
service_name="generic-service",
resource_type="generic-type",
resource_tags={"team": "platform"},
resource_metadata={"owner": "security"},
resource_details={"id": "generic-resource"},
partition="aws",
raw={},
compliance={},
metadata={"categories": ["security"], "resourcegroup": "identity"},
muted=False,
)
cached_resource_finding = FakeFinding(
uid="finding-cache-miss-retry-restores-dirty-resource",
status=StatusChoices.FAIL,
status_extended="cached resource changed",
severity=Severity.high,
check_id="generic_cached_resource_check",
resource_uid=cached_resource.uid,
resource_name="new-cached-resource",
region="eu-west-1",
service_name="new-service",
resource_type="new-type",
resource_tags={},
resource_metadata={"owner": "platform"},
resource_details={"id": "cached-resource"},
partition="aws",
raw={},
compliance={},
metadata={"categories": ["security"], "resourcegroup": "identity"},
muted=False,
)
resource_cache = CacheMissAfterPreResolve(resource_uid)
resource_cache[cached_resource.uid] = cached_resource
tag_cache = {}
resource_failed_findings_cache = {cached_resource.uid: 0}
scan_resource_cache: set[tuple[str, str, str, str]] = set()
scan_categories_cache: dict[tuple[str, str], dict[str, int]] = {}
scan_resource_groups_cache: dict[tuple[str, str], dict[str, int]] = {}
group_resources_cache: dict[str, set] = {}
original_bulk_create = ResourceFindingMapping.objects.bulk_create
original_tag_mapping_bulk_create = ResourceTagMapping.objects.bulk_create
mapping_bulk_create_calls = []
tag_mapping_bulk_create_calls = []
def fail_once_then_bulk_create(objects, *args, **kwargs):
mapping_bulk_create_calls.append([str(obj.resource_id) for obj in objects])
if len(mapping_bulk_create_calls) == 1:
raise IntegrityError("rollback after fallback resource creation")
return original_bulk_create(objects, *args, **kwargs)
def track_tag_mappings_bulk_create(objects, *args, **kwargs):
tag_mapping_bulk_create_calls.append([str(obj.tag_id) for obj in objects])
return original_tag_mapping_bulk_create(objects, *args, **kwargs)
with (
patch("tasks.jobs.scan.CELERY_DEADLOCK_ATTEMPTS", 2),
patch("tasks.jobs.scan.rls_transaction", new=atomic_rls_transaction),
patch("api.db_utils.rls_transaction", new=atomic_rls_transaction),
patch.object(
ResourceTagMapping.objects,
"bulk_create",
side_effect=track_tag_mappings_bulk_create,
),
patch.object(
ResourceFindingMapping.objects,
"bulk_create",
side_effect=fail_once_then_bulk_create,
),
):
_process_finding_micro_batch(
str(tenant.id),
[finding, cached_resource_finding],
scan,
provider,
resource_cache,
tag_cache,
{},
resource_failed_findings_cache,
set(),
scan_resource_cache,
{},
scan_categories_cache,
scan_resource_groups_cache,
group_resources_cache,
)
resource = Resource.objects.get(
tenant_id=tenant.id,
provider_id=provider.id,
uid=resource_uid,
)
created_finding = Finding.objects.get(
tenant_id=tenant.id,
scan_id=scan.id,
uid=finding.uid,
)
cached_resource.refresh_from_db()
assert len(mapping_bulk_create_calls) == 2
assert mapping_bulk_create_calls[0] != mapping_bulk_create_calls[1]
assert len(tag_mapping_bulk_create_calls) == 2
assert tag_mapping_bulk_create_calls[0] != tag_mapping_bulk_create_calls[1]
assert created_finding.scan_id == scan.id
assert resource.findings.filter(
tenant_id=tenant.id,
scan_id=scan.id,
uid=finding.uid,
).exists()
assert cached_resource.findings.filter(
tenant_id=tenant.id,
scan_id=scan.id,
uid=cached_resource_finding.uid,
).exists()
assert cached_resource.name == cached_resource_finding.resource_name
assert cached_resource.region == cached_resource_finding.region
assert cached_resource.service == cached_resource_finding.service_name
assert cached_resource.type == cached_resource_finding.resource_type
assert resource_cache[resource_uid].id == resource.id
assert resource_failed_findings_cache[resource_uid] == 1
assert resource_failed_findings_cache[cached_resource.uid] == 1
assert scan_resource_cache == {
(
str(resource.id),
finding.service_name,
finding.region,
finding.resource_type,
),
(
str(cached_resource.id),
cached_resource_finding.service_name,
cached_resource_finding.region,
cached_resource_finding.resource_type,
),
}
assert (
tag_cache[("team", "platform")].id
== ResourceTag.objects.get(
tenant_id=tenant.id,
key="team",
value="platform",
).id
)
assert scan_categories_cache == {
("security", "high"): {"total": 2, "failed": 2, "new_failed": 2}
}
assert scan_resource_groups_cache == {
("identity", "high"): {"total": 2, "failed": 2, "new_failed": 2}
}
assert group_resources_cache == {
"identity": {resource_uid, cached_resource.uid}
}
def test_process_finding_micro_batch_propagates_retryable_cache_miss_db_errors(
self, tenants_fixture, scans_fixture
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = scan.provider
resource_uid = "arn:aws:securityhub:us-east-1:123456789012:hub/retryable"
finding = FakeFinding(
uid="finding-cache-miss-retryable-error",
status=StatusChoices.FAIL,
status_extended="missing hub",
severity=Severity.high,
check_id="securityhub_enabled",
resource_uid=resource_uid,
resource_name="hub/retryable",
region="us-east-1",
service_name="securityhub",
resource_type="hub",
resource_tags={},
resource_metadata={},
resource_details={},
partition="aws",
raw={},
compliance={},
metadata={},
muted=False,
)
with (
patch("tasks.jobs.scan.CELERY_DEADLOCK_ATTEMPTS", 1),
patch.object(
Resource.objects,
"create",
side_effect=OperationalError("deadlock detected"),
),
):
with pytest.raises(OperationalError, match="deadlock detected"):
self._process_one_finding_micro_batch(
tenant,
scan,
provider,
finding,
resource_cache=CacheMissAfterPreResolve(resource_uid),
)
assert not Finding.objects.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
).exists()
def test_process_finding_micro_batch_propagates_unrecovered_cache_miss_integrity_error(
self, tenants_fixture, scans_fixture
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = scan.provider
resource_uid = "arn:aws:securityhub:us-east-1:123456789012:hub/unrecovered"
finding = FakeFinding(
uid="finding-cache-miss-unrecovered-integrity-error",
status=StatusChoices.FAIL,
status_extended="missing hub",
severity=Severity.high,
check_id="securityhub_enabled",
resource_uid=resource_uid,
resource_name="hub/unrecovered",
region="us-east-1",
service_name="securityhub",
resource_type="hub",
resource_tags={},
resource_metadata={},
resource_details={},
partition="aws",
raw={},
compliance={},
metadata={},
muted=False,
)
original_resource_filter = Resource.objects.filter
resource_filter_result = MagicMock()
resource_filter_result.first.side_effect = [None, None]
def resource_filter_side_effect(*args, **kwargs):
if kwargs.get("uid") == resource_uid:
return resource_filter_result
return original_resource_filter(*args, **kwargs)
with (
patch("tasks.jobs.scan.CELERY_DEADLOCK_ATTEMPTS", 1),
patch.object(
Resource.objects,
"filter",
side_effect=resource_filter_side_effect,
),
patch.object(
Resource.objects,
"create",
side_effect=IntegrityError("constraint violation"),
),
):
with pytest.raises(IntegrityError, match="constraint violation"):
self._process_one_finding_micro_batch(
tenant,
scan,
provider,
finding,
resource_cache=CacheMissAfterPreResolve(resource_uid),
)
assert not Finding.objects.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
).exists()
def test_process_finding_micro_batch_creates_records_and_updates_caches(
self, tenants_fixture, scans_fixture
):
@@ -1573,8 +2179,12 @@ class TestProcessFindingMicroBatch:
group_resources_cache,
)
created_finding = Finding.objects.get(uid=finding.uid)
resource = Resource.objects.get(uid=finding.resource_uid)
created_finding = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
)
resource = Resource.objects.get(
tenant_id=tenant.id, provider_id=provider.id, uid=finding.resource_uid
)
assert created_finding.scan_id == scan.id
assert created_finding.status == StatusChoices.PASS
@@ -1602,7 +2212,9 @@ class TestProcessFindingMicroBatch:
assert set(resource.tags.values_list("key", "value")) == set(
finding.resource_tags.items()
)
assert resource.findings.filter(uid=finding.uid).exists()
assert resource.findings.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
).exists()
assert resource_cache[finding.resource_uid].id == resource.id
assert resource_failed_findings_cache[finding.resource_uid] == 0
@@ -1691,7 +2303,9 @@ class TestProcessFindingMicroBatch:
)
existing_resource.refresh_from_db()
created_finding = Finding.objects.get(uid=finding.uid)
created_finding = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
)
assert created_finding.delta == Finding.DeltaChoices.CHANGED
assert created_finding.status == StatusChoices.FAIL
@@ -1725,7 +2339,9 @@ class TestProcessFindingMicroBatch:
assert set(existing_resource.tags.values_list("key", "value")) == {
("team", "devsec")
}
assert existing_resource.findings.filter(uid=finding.uid).exists()
assert existing_resource.findings.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
).exists()
assert resource_cache[finding.resource_uid].region == finding.region
assert resource_cache[finding.resource_uid].service == finding.service_name
@@ -1891,10 +2507,14 @@ class TestProcessFindingMicroBatch:
)
# Verify the long UID finding was NOT created
assert not Finding.objects.filter(uid=long_uid).exists()
assert not Finding.objects.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=long_uid
).exists()
# Verify the normal finding WAS created
assert Finding.objects.filter(uid=normal_finding.uid).exists()
assert Finding.objects.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=normal_finding.uid
).exists()
# Verify logging was called for skipped finding
assert mock_logger.warning.called
@@ -2019,8 +2639,12 @@ class TestProcessFindingMicroBatch:
"new_failed": 1,
}
created_finding1 = Finding.objects.get(uid="finding-cat-1")
created_finding2 = Finding.objects.get(uid="finding-cat-2")
created_finding1 = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid="finding-cat-1"
)
created_finding2 = Finding.objects.get(
tenant_id=tenant.id, scan_id=scan.id, uid="finding-cat-2"
)
assert set(created_finding1.categories) == {"gen-ai", "security"}
assert set(created_finding2.categories) == {"security", "iam"}
Generated
+3 -3
View File
@@ -4674,7 +4674,7 @@ wheels = [
[[package]]
name = "prowler"
version = "5.35.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#f5ea116763aeffede9f399c8934fc280eaccd315" }
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.35#1459046985908c099bd2b2e279530e8a47bccdf8" }
dependencies = [
{ name = "alibabacloud-actiontrail20200706" },
{ name = "alibabacloud-credentials" },
@@ -4762,7 +4762,7 @@ dependencies = [
[[package]]
name = "prowler-api"
version = "1.36.0"
version = "1.36.1"
source = { virtual = "." }
dependencies = [
{ name = "cartography" },
@@ -4862,7 +4862,7 @@ requires-dist = [
{ name = "matplotlib", specifier = "==3.10.8" },
{ name = "neo4j", specifier = "==6.1.0" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=master" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.35" },
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" },
{ name = "reportlab", specifier = "==4.4.10" },
@@ -0,0 +1 @@
Alibaba Cloud SSH and RDP security group checks no longer produce false negatives when allowed rules use capitalized `Policy="Accept"` values
+1 -1
View File
@@ -49,7 +49,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.35.0"
prowler_version = "5.35.1"
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"
@@ -26,7 +26,7 @@ class ecs_securitygroup_restrict_rdp_internet(Check):
for ingress_rule in security_group.ingress_rules:
# Check if rule allows traffic (policy == "accept")
if ingress_rule.get("policy", "accept") != "accept":
if str(ingress_rule.get("policy", "accept")).lower() != "accept":
continue
# Check protocol (tcp for RDP)
@@ -26,7 +26,7 @@ class ecs_securitygroup_restrict_ssh_internet(Check):
for ingress_rule in security_group.ingress_rules:
# Check if rule allows traffic (policy == "accept")
if ingress_rule.get("policy", "accept") != "accept":
if str(ingress_rule.get("policy", "accept")).lower() != "accept":
continue
# Check protocol (tcp for SSH)
+1 -1
View File
@@ -125,7 +125,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
name = "prowler"
readme = "README.md"
requires-python = ">=3.10,<3.14"
version = "5.35.0"
version = "5.35.1"
[project.scripts]
prowler = "prowler.__main__:prowler"
@@ -37,7 +37,7 @@ class TestEcsSecurityGroupRestrictRdpInternet:
"ip_protocol": "tcp",
"source_cidr_ip": "0.0.0.0/0",
"port_range": "3389/3389",
"policy": "accept",
"policy": "Accept",
}
],
)
@@ -80,7 +80,7 @@ class TestEcsSecurityGroupRestrictRdpInternet:
"ip_protocol": "tcp",
"source_cidr_ip": "10.0.0.0/24",
"port_range": "3389/3389",
"policy": "accept",
"policy": "Accept",
}
],
)
@@ -37,7 +37,7 @@ class TestEcsSecurityGroupRestrictSSHInternet:
"ip_protocol": "tcp",
"source_cidr_ip": "0.0.0.0/0",
"port_range": "22/22",
"policy": "accept",
"policy": "Accept",
}
],
)
@@ -81,7 +81,7 @@ class TestEcsSecurityGroupRestrictSSHInternet:
"ip_protocol": "tcp",
"source_cidr_ip": "10.0.0.0/24",
"port_range": "22/22",
"policy": "accept",
"policy": "Accept",
}
],
)
+3 -1
View File
@@ -4,7 +4,9 @@ FROM node:24.13.0-alpine@sha256:cd6fb7efa6490f039f3471a189214d5f548c11df1ff9e5b1
LABEL maintainer="https://github.com/prowler-cloud"
# Patch Alpine OpenSSL runtime packages before all stages inherit the base image.
RUN apk upgrade --no-cache libcrypto3 libssl3 && corepack enable
# The build uses pnpm via corepack, so npm is unused — remove it (and npx) to drop
# the bundled-npm CVE surface (node-tar CVE-2026-59873) from every stage, incl. prod.
RUN apk upgrade --no-cache libcrypto3 libssl3 && corepack enable && rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
# Install dependencies only when needed
FROM base AS deps
@@ -0,0 +1 @@
Billing navigation is hidden when Cloud billing is disabled, including Enterprise deployments
@@ -0,0 +1 @@
Sidebar logo top spacing in the main app sidebar
@@ -0,0 +1 @@
Removed the unused `npm` CLI from the UI container image, eliminating the bundled `node-tar` `CVE-2026-59873` (and future bundled-npm CVEs); the image builds with `pnpm` via `corepack` and does not use `npm`
@@ -0,0 +1 @@
Removed leftover Git merge-conflict markers accidentally committed to `ui/package.json` and `ui/pnpm-lock.yaml` during the vitest 4.1.10 backport, restoring valid manifests
@@ -0,0 +1 @@
Bumped `vitest` and `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/coverage-v8` from `4.1.8` to `4.1.10`, resolving the critical `@vitest/browser` Browser Mode file-access permission bypass (`GHSA-p63j-vcc4-9vmv`) flagged by `pnpm audit`; dev dependencies only, no runtime impact
@@ -24,15 +24,20 @@ interface AppSidebarContentProps {
export function AppSidebarContent({ onSelect }: AppSidebarContentProps) {
const pathname = usePathname();
const { permissions } = useAuth();
const { apiDocsUrl } = useRuntimeConfig();
const { apiDocsUrl, cloudBillingEnabled } = useRuntimeConfig();
const mode = useAppSidebarMode((state) => state.mode);
const isCloudEnvironment = isCloud();
const sections = getNavigationConfig({ pathname, apiDocsUrl, permissions });
const sections = getNavigationConfig({
pathname,
apiDocsUrl,
cloudBillingEnabled,
permissions,
});
const showChat = isCloudEnvironment && mode === APP_SIDEBAR_MODE.CHAT;
return (
<div className="relative flex h-full min-h-0 w-full flex-col overflow-hidden">
<div className="shrink-0 px-5 pt-6 pb-7">
<div className="shrink-0 px-5 pt-8 pb-7">
<Link
href="/"
aria-label="Prowler home"
@@ -157,6 +157,7 @@ describe("getNavigationConfig", () => {
const billing = getNavigationConfig({
pathname: "/billing",
apiDocsUrl: null,
cloudBillingEnabled: true,
permissions,
})
.flatMap((section) => section.items)
@@ -183,17 +184,28 @@ describe("getNavigationConfig", () => {
const cloudItems = getNavigationConfig({
pathname: "/",
apiDocsUrl: null,
cloudBillingEnabled: true,
permissions,
}).flatMap((section) => section.items);
const enterpriseItems = getNavigationConfig({
pathname: "/",
apiDocsUrl: null,
cloudBillingEnabled: false,
permissions: { ...permissions, manage_billing: true },
}).flatMap((section) => section.items);
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
const localItems = getNavigationConfig({
pathname: "/",
apiDocsUrl: null,
cloudBillingEnabled: true,
permissions: { ...permissions, manage_billing: true },
}).flatMap((section) => section.items);
// Then
expect(cloudItems.find((item) => item.label === "Billing")).toBeUndefined();
expect(
enterpriseItems.find((item) => item.label === "Billing"),
).toBeUndefined();
expect(localItems.find((item) => item.label === "Billing")).toBeUndefined();
});
@@ -31,6 +31,7 @@ import {
interface NavigationConfigOptions {
pathname: string;
apiDocsUrl?: string | null;
cloudBillingEnabled?: boolean;
permissions?: RolePermissionAttributes;
}
@@ -106,6 +107,7 @@ export function filterNavigationByPermissions(
export function getNavigationConfig({
pathname,
apiDocsUrl = null,
cloudBillingEnabled = false,
permissions,
}: NavigationConfigOptions): NavigationSection[] {
const isCloudEnvironment = isCloud();
@@ -265,7 +267,7 @@ export function getNavigationConfig({
},
],
},
...(isCloudEnvironment
...(isCloudEnvironment && cloudBillingEnabled
? [
{
kind: NAVIGATION_ITEM_KIND.LINK,
+4 -4
View File
@@ -135,9 +135,9 @@
"@typescript-eslint/eslint-plugin": "8.53.0",
"@typescript-eslint/parser": "8.53.0",
"@vitejs/plugin-react": "5.1.2",
"@vitest/browser": "4.1.8",
"@vitest/browser-playwright": "4.1.8",
"@vitest/coverage-v8": "4.1.8",
"@vitest/browser": "4.1.10",
"@vitest/browser-playwright": "4.1.10",
"@vitest/coverage-v8": "4.1.10",
"babel-plugin-react-compiler": "1.0.0",
"dotenv": "16.6.1",
"dotenv-expand": "12.0.3",
@@ -160,7 +160,7 @@
"prettier-plugin-tailwindcss": "0.6.14",
"tailwindcss": "4.1.18",
"typescript": "5.5.4",
"vitest": "4.1.8",
"vitest": "4.1.10",
"vitest-browser-react": "2.0.4"
},
"packageManager": "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d",
+87 -91
View File
@@ -338,14 +338,14 @@ importers:
specifier: 5.1.2
version: 5.1.2(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
'@vitest/browser':
specifier: 4.1.8
version: 4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.8)
specifier: 4.1.10
version: 4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/browser-playwright':
specifier: 4.1.8
version: 4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(playwright@1.56.1)(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.8)
specifier: 4.1.10
version: 4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(playwright@1.56.1)(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/coverage-v8':
specifier: 4.1.8
version: 4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8)
specifier: 4.1.10
version: 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10)
babel-plugin-react-compiler:
specifier: 1.0.0
version: 1.0.0
@@ -413,11 +413,11 @@ importers:
specifier: 5.5.4
version: 5.5.4
vitest:
specifier: 4.1.8
version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
specifier: 4.1.10
version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
vitest-browser-react:
specifier: 2.0.4
version: 2.0.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.8)
version: 2.0.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.10)
packages:
@@ -3578,31 +3578,31 @@ packages:
peerDependencies:
vite: 7.3.5
'@vitest/browser-playwright@4.1.8':
resolution: {integrity: sha512-SR7FqgegaexEg73xvf3ArtygXegagMdXnL0EZMpxrWvvhQxvicD/E8p0ib0J91riPRtQUViyh67Xjw3NqvyhVg==}
'@vitest/browser-playwright@4.1.10':
resolution: {integrity: sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==}
peerDependencies:
playwright: '*'
vitest: 4.1.8
vitest: 4.1.10
'@vitest/browser@4.1.8':
resolution: {integrity: sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==}
'@vitest/browser@4.1.10':
resolution: {integrity: sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==}
peerDependencies:
vitest: 4.1.8
vitest: 4.1.10
'@vitest/coverage-v8@4.1.8':
resolution: {integrity: sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==}
'@vitest/coverage-v8@4.1.10':
resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==}
peerDependencies:
'@vitest/browser': 4.1.8
vitest: 4.1.8
'@vitest/browser': 4.1.10
vitest: 4.1.10
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@4.1.8':
resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==}
'@vitest/expect@4.1.10':
resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
'@vitest/mocker@4.1.8':
resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==}
'@vitest/mocker@4.1.10':
resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
peerDependencies:
msw: ^2.4.9
vite: 7.3.5
@@ -3612,20 +3612,20 @@ packages:
vite:
optional: true
'@vitest/pretty-format@4.1.8':
resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==}
'@vitest/pretty-format@4.1.10':
resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
'@vitest/runner@4.1.8':
resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==}
'@vitest/runner@4.1.10':
resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
'@vitest/snapshot@4.1.8':
resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==}
'@vitest/snapshot@4.1.10':
resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
'@vitest/spy@4.1.8':
resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==}
'@vitest/spy@4.1.10':
resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
'@vitest/utils@4.1.8':
resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==}
'@vitest/utils@4.1.10':
resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
@@ -6949,20 +6949,20 @@ packages:
'@types/react-dom':
optional: true
vitest@4.1.8:
resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==}
vitest@4.1.10:
resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 4.1.8
'@vitest/browser-preview': 4.1.8
'@vitest/browser-webdriverio': 4.1.8
'@vitest/coverage-istanbul': 4.1.8
'@vitest/coverage-v8': 4.1.8
'@vitest/ui': 4.1.8
'@vitest/browser-playwright': 4.1.10
'@vitest/browser-preview': 4.1.10
'@vitest/browser-webdriverio': 4.1.10
'@vitest/coverage-istanbul': 4.1.10
'@vitest/coverage-v8': 4.1.10
'@vitest/ui': 4.1.10
happy-dom: '*'
jsdom: '*'
vite: 7.3.5
@@ -9583,10 +9583,10 @@ snapshots:
'@rollup/pluginutils': 5.3.0(rollup@4.59.0)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.4)
fdir: 6.5.0(picomatch@4.0.5)
is-reference: 1.2.1
magic-string: 0.30.21
picomatch: 4.0.4
picomatch: 4.0.5
optionalDependencies:
rollup: 4.59.0
@@ -9594,7 +9594,7 @@ snapshots:
dependencies:
'@types/estree': 1.0.9
estree-walker: 2.0.2
picomatch: 4.0.4
picomatch: 4.0.5
optionalDependencies:
rollup: 4.59.0
@@ -10538,7 +10538,7 @@ snapshots:
debug: 4.4.3
minimatch: 9.0.7
semver: 7.8.0
tinyglobby: 0.2.16
tinyglobby: 0.2.17
ts-api-utils: 2.4.0(typescript@5.5.4)
typescript: 5.5.4
transitivePeerDependencies:
@@ -10608,29 +10608,29 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/browser-playwright@4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(playwright@1.56.1)(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.8)':
'@vitest/browser-playwright@4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(playwright@1.56.1)(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.10)':
dependencies:
'@vitest/browser': 4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.8)
'@vitest/mocker': 4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
'@vitest/browser': 4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/mocker': 4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
playwright: 1.56.1
tinyrainbow: 3.1.0
vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
transitivePeerDependencies:
- bufferutil
- msw
- utf-8-validate
- vite
'@vitest/browser@4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.8)':
'@vitest/browser@4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.10)':
dependencies:
'@blazediff/core': 1.9.1
'@vitest/mocker': 4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
'@vitest/utils': 4.1.8
'@vitest/mocker': 4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
'@vitest/utils': 4.1.10
magic-string: 0.30.21
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.1.0
vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
@@ -10638,10 +10638,10 @@ snapshots:
- utf-8-validate
- vite
'@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8)':
'@vitest/coverage-v8@4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.8
'@vitest/utils': 4.1.10
ast-v8-to-istanbul: 1.0.3
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
@@ -10650,49 +10650,49 @@ snapshots:
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
optionalDependencies:
'@vitest/browser': 4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.8)
'@vitest/browser': 4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/expect@4.1.8':
'@vitest/expect@4.1.10':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@vitest/spy': 4.1.8
'@vitest/utils': 4.1.8
'@vitest/spy': 4.1.10
'@vitest/utils': 4.1.10
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))':
'@vitest/mocker@4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.8
'@vitest/spy': 4.1.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
msw: 2.13.4(@types/node@24.10.8)(typescript@5.5.4)
vite: 7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0)
'@vitest/pretty-format@4.1.8':
'@vitest/pretty-format@4.1.10':
dependencies:
tinyrainbow: 3.1.0
'@vitest/runner@4.1.8':
'@vitest/runner@4.1.10':
dependencies:
'@vitest/utils': 4.1.8
'@vitest/utils': 4.1.10
pathe: 2.0.3
'@vitest/snapshot@4.1.8':
'@vitest/snapshot@4.1.10':
dependencies:
'@vitest/pretty-format': 4.1.8
'@vitest/utils': 4.1.8
'@vitest/pretty-format': 4.1.10
'@vitest/utils': 4.1.10
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@4.1.8': {}
'@vitest/spy@4.1.10': {}
'@vitest/utils@4.1.8':
'@vitest/utils@4.1.10':
dependencies:
'@vitest/pretty-format': 4.1.8
'@vitest/pretty-format': 4.1.10
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
@@ -11931,10 +11931,6 @@ snapshots:
dependencies:
walk-up-path: 4.0.0
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fdir@6.5.0(picomatch@4.0.5):
optionalDependencies:
picomatch: 4.0.5
@@ -14397,8 +14393,8 @@ snapshots:
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
fdir: 6.5.0(picomatch@4.0.5)
picomatch: 4.0.5
tinyglobby@0.2.17:
dependencies:
@@ -14645,42 +14641,42 @@ snapshots:
terser: 5.49.0
yaml: 2.9.0
vitest-browser-react@2.0.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.8):
vitest-browser-react@2.0.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.10):
dependencies:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
optionalDependencies:
'@types/react': 19.2.17
'@types/react-dom': 19.2.3(@types/react@19.2.17)
vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0)):
vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.8)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
'@vitest/spy': 4.1.8
'@vitest/utils': 4.1.8
'@vitest/expect': 4.1.10
'@vitest/mocker': 4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.10
'@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10
'@vitest/spy': 4.1.10
'@vitest/utils': 4.1.10
es-module-lexer: 2.3.0
expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.1
pathe: 2.0.3
picomatch: 4.0.4
picomatch: 4.0.5
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.1.2
tinyglobby: 0.2.16
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.1
'@types/node': 24.10.8
'@vitest/browser-playwright': 4.1.8(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(playwright@1.56.1)(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.8)
'@vitest/coverage-v8': 4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8)
'@vitest/browser-playwright': 4.1.10(msw@2.13.4(@types/node@24.10.8)(typescript@5.5.4))(playwright@1.56.1)(vite@7.3.5(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10)
jsdom: 27.4.0
transitivePeerDependencies:
- msw
+10 -4
View File
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import type { NextAuthRequest } from "next-auth";
import { auth } from "@/auth.config";
import { readEnv } from "@/lib/runtime-env";
const publicRoutes = [
"/sign-in",
@@ -23,6 +24,8 @@ export default auth((req: NextAuthRequest) => {
const user = req.auth?.user;
const sessionError = req.auth?.error;
const cloudBillingEnabled =
(readEnv("CLOUD_BILLING_ENABLED") ?? "false") !== "false";
// If there's a session error (e.g., RefreshAccessTokenError), redirect to login with error info
if (sessionError && !isPublicRoute(pathname)) {
@@ -38,13 +41,16 @@ export default auth((req: NextAuthRequest) => {
return NextResponse.redirect(signInUrl);
}
if (
pathname.startsWith("/billing") &&
(!cloudBillingEnabled || user?.permissions?.manage_billing !== true)
) {
return NextResponse.redirect(new URL("/profile", req.url));
}
if (user?.permissions) {
const permissions = user.permissions;
if (pathname.startsWith("/billing") && !permissions.manage_billing) {
return NextResponse.redirect(new URL("/profile", req.url));
}
if (
pathname.startsWith("/integrations") &&
!permissions.manage_integrations
Generated
+1 -1
View File
@@ -3553,7 +3553,7 @@ wheels = [
[[package]]
name = "prowler"
version = "5.35.0"
version = "5.35.1"
source = { editable = "." }
dependencies = [
{ name = "alibabacloud-actiontrail20200706" },