diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 98cbcaa6..52d5c38b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1 @@ open_collective: intelowl-project -github: intelowlproject \ No newline at end of file diff --git a/README.md b/README.md index b1452bf9..dbdd1822 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,12 @@ To install it locally, Please refer to our [installation guide](https://intelowl Thanks to [The Honeynet Project](https://www.honeynet.org) we are providing free public feeds available [here](https://greedybear.honeynet.org). -#### DigitalOcean +#### Google Summer of Code + GSoC logo -In 2022 we joined the official [DigitalOcean Open Source Program](https://www.digitalocean.com/open-source?utm_medium=opensource&utm_source=IntelOwl). +In 2026 we started participating to the [Google Summer of Code](https://summerofcode.withgoogle.com/) (GSoC)! +If you are interested in participating in the next Google Summer of Code, check all the info available in the [dedicated repository](https://github.com/intelowlproject/gsoc)! ## Maintainers and Key Contributors diff --git a/api/serializers.py b/api/serializers.py index aba8d0b1..f11298b6 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -6,15 +6,15 @@ from rest_framework import serializers from greedybear.consts import REGEX_DOMAIN -from greedybear.models import IOC, GeneralHoneypot, Sensor, Tag +from greedybear.models import IOC, Honeypot, Sensor, Tag from greedybear.utils import is_ip_address logger = logging.getLogger(__name__) -class GeneralHoneypotSerializer(serializers.ModelSerializer): +class HoneypotSerializer(serializers.ModelSerializer): class Meta: - model = GeneralHoneypot + model = Honeypot def to_representation(self, value): return value.name @@ -33,7 +33,7 @@ class Meta: class IOCSerializer(serializers.ModelSerializer): - general_honeypot = GeneralHoneypotSerializer(many=True, read_only=True) + general_honeypot = HoneypotSerializer(many=True, read_only=True, source="honeypots") tags = TagSerializer(many=True, read_only=True) sensors = SensorSerializer(many=True, read_only=True) diff --git a/api/views/__init__.py b/api/views/__init__.py index ebc2b1f2..398982a0 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -2,7 +2,7 @@ from api.views.cowrie_session import * from api.views.enrichment import * from api.views.feeds import * -from api.views.general_honeypot import * from api.views.health import * +from api.views.honeypots import * from api.views.news import * from api.views.statistics import * diff --git a/api/views/health.py b/api/views/health.py index ac3f2533..e6ef70c0 100644 --- a/api/views/health.py +++ b/api/views/health.py @@ -15,7 +15,7 @@ IOC, CowrieSession, FireHolList, - GeneralHoneypot, + Honeypot, MassScanner, TorExitNode, ) @@ -71,8 +71,8 @@ def get_observables_overview(last_24h): ) honeypot_stats = { - "total": GeneralHoneypot.objects.count(), - "active": GeneralHoneypot.objects.filter(active=True).count(), + "total": Honeypot.objects.count(), + "active": Honeypot.objects.filter(active=True).count(), } threat_list_stats = { diff --git a/api/views/general_honeypot.py b/api/views/honeypots.py similarity index 62% rename from api/views/general_honeypot.py rename to api/views/honeypots.py index 7679eb04..ea2d5148 100644 --- a/api/views/general_honeypot.py +++ b/api/views/honeypots.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from greedybear.consts import GET -from greedybear.models import GeneralHoneypot +from greedybear.models import Honeypot logger = logging.getLogger(__name__) @@ -23,14 +23,14 @@ def general_honeypot_list(request): Response: A JSON response containing the list of general honeypots. """ - logger.info(f"Requested general honeypots list from {request.user}.") + logger.info(f"Requested honeypots list from {request.user}.") active = request.query_params.get("onlyActive") honeypots = [] - general_honeypots = GeneralHoneypot.objects.all() + honeypot_objs = Honeypot.objects.all() if active == "true": - general_honeypots = general_honeypots.filter(active=True) - logger.info(f"Requested only active general honeypots from {request.user}") - honeypots.extend([hp.name for hp in general_honeypots]) + honeypot_objs = honeypot_objs.filter(active=True) + logger.info(f"Requested only active honeypots from {request.user}") + honeypots.extend([hp.name for hp in honeypot_objs]) - logger.info(f"General honeypots: {honeypots} given back to user {request.user}") + logger.info(f"Honeypots: {honeypots} given back to user {request.user}") return Response(honeypots) diff --git a/api/views/statistics.py b/api/views/statistics.py index adea971b..d4bd1f75 100644 --- a/api/views/statistics.py +++ b/api/views/statistics.py @@ -10,7 +10,7 @@ from rest_framework.decorators import action from rest_framework.response import Response -from greedybear.models import IOC, GeneralHoneypot, Statistics, ViewType +from greedybear.models import IOC, Honeypot, Statistics, ViewType logger = logging.getLogger(__name__) @@ -92,7 +92,7 @@ def countries(self, request): qs = ( IOC.objects.filter(last_seen__gte=delta) .exclude(attacker_country="") - .filter(general_honeypot__active=True) + .filter(honeypots__active=True) .values("attacker_country") .annotate(count=Count("id", distinct=True)) .order_by("-count") @@ -103,7 +103,7 @@ def countries(self, request): @action(detail=False, methods=["get"]) def feeds_types(self, request): """ - Retrieve statistics for different types of feeds using GeneralHoneypot M2M relationship. + Retrieve statistics for different types of feeds using Honeypot M2M relationship. Args: request: The incoming request object. @@ -113,10 +113,10 @@ def feeds_types(self, request): """ # Build annotations for each active general honeypot annotations = {} - general_honeypots = GeneralHoneypot.objects.all().filter(active=True) - for hp in general_honeypots: + honeypots = Honeypot.objects.all().filter(active=True) + for hp in honeypots: # Use M2M relationship instead of boolean fields - annotations[hp.name] = Count("name", distinct=True, filter=Q(general_honeypot__name__iexact=hp.name)) + annotations[hp.name] = Count("name", distinct=True, filter=Q(honeypots__name__iexact=hp.name)) return self.__aggregation_response_static_ioc(annotations) def __aggregation_response_static_statistics(self, annotations: dict) -> Response: @@ -147,7 +147,7 @@ def __aggregation_response_static_ioc(self, annotations: dict) -> Response: qs = ( IOC.objects.filter(last_seen__gte=delta) - .exclude(general_honeypot__active=False) + .exclude(honeypots__active=False) .annotate(date=Trunc("last_seen", basis)) .values("date") .annotate(**annotations) diff --git a/api/views/utils.py b/api/views/utils.py index 349e60e8..109d515c 100644 --- a/api/views/utils.py +++ b/api/views/utils.py @@ -1,14 +1,16 @@ # This file is a part of GreedyBear https://github.com/honeynet/GreedyBear # See the file 'LICENSE' for copying permission. import csv +import hashlib import logging +import urllib.parse from datetime import datetime, timedelta import feedparser import requests from django.conf import settings from django.contrib.postgres.aggregates import ArrayAgg -from django.core.cache import cache +from django.core.cache import cache, caches from django.db.models import Count, F, Max, Min, Q, Sum, Value from django.db.models.functions import JSONObject from django.http import HttpResponse, HttpResponseBadRequest, StreamingHttpResponse @@ -19,7 +21,7 @@ from api.serializers import FeedsRequestSerializer, parse_feed_types from greedybear.consts import CACHE_KEY_GREEDYBEAR_NEWS, CACHE_TIMEOUT_SECONDS, RSS_FEED_URL from greedybear.enums import IpReputation -from greedybear.models import IOC, GeneralHoneypot, Statistics +from greedybear.models import IOC, Honeypot, Statistics from greedybear.utils import is_ip_address, is_valid_domain logger = logging.getLogger(__name__) @@ -144,8 +146,8 @@ def get_valid_feed_types() -> frozenset[str]: Returns: frozenset[str]: An immutable set of valid feed type strings """ - general_honeypots = GeneralHoneypot.objects.filter(active=True) - feed_types = ["all"] + [hp.name.lower() for hp in general_honeypots] + honeypots = Honeypot.objects.filter(active=True) + feed_types = ["all"] + [hp.name.lower() for hp in honeypots] return frozenset(feed_types) @@ -226,13 +228,13 @@ def get_queryset(request, feed_params, valid_feed_types, is_aggregated=False, se if "all" not in feed_params.feed_types: type_filter = Q() for ft in feed_params.feed_types: - type_filter |= Q(general_honeypot__name__iexact=ft) + type_filter |= Q(honeypots__name__iexact=ft) iocs = iocs.filter(type_filter) # aggregated feeds calculate metrics differently and need all rows to be accurate. if not is_aggregated: - iocs = iocs.filter(general_honeypot__active=True) - iocs = iocs.annotate(honeypots=ArrayAgg("general_honeypot__name", distinct=True)) + iocs = iocs.filter(honeypots__active=True) + iocs = iocs.annotate(honeypot_names=ArrayAgg("honeypots__name", distinct=True)) # Only annotate tags metadata when the response format needs it (e.g. JSON), # to avoid unnecessary joins and aggregation work for txt/csv feeds. if getattr(feed_params, "format", "").lower() == "json": @@ -315,7 +317,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N "login_attempts", "recurrence_probability", "expected_interactions", - "honeypots", # used to build feed_type; removed from response + "honeypot_names", # used to build feed_type; removed from response "destination_ports", # used to calculate destination_port_count "attacker_country", "autonomous_system", @@ -344,7 +346,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N else: iocs_iter = iocs.values(*required_fields).iterator(chunk_size=2000) for ioc in iocs_iter: - ioc_feed_type = [hp.lower() for hp in ioc.get("honeypots", []) if hp] + ioc_feed_type = [hp.lower() for hp in ioc.get("honeypot_names", []) if hp] data_ = ioc | { "first_seen": ioc["first_seen"].strftime("%Y-%m-%d"), @@ -358,7 +360,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N if not verbose: data_.pop("destination_ports", None) data_.pop("autonomous_system", None) - data_.pop("honeypots", None) + data_.pop("honeypot_names", None) data_.pop("id", None) json_list.append(data_) @@ -386,7 +388,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N "first_seen", "last_seen", "recurrence_probability", - "honeypots", + "honeypot_names", "ip_reputation", } # Fetch fields from database @@ -416,7 +418,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N confidence = 90 # Labels - labels = [hp.lower() for hp in ioc.get("honeypots", []) if hp] + labels = [hp.lower() for hp in ioc.get("honeypot_names", []) if hp] if ioc.get("ip_reputation"): labels.append(ioc["ip_reputation"]) @@ -446,15 +448,33 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N def asn_aggregated_queryset(iocs_qs, request, feed_params): """ - Perform DB-level aggregation grouped by ASN. + Retrieve ASN aggregation data. Caches the heavy aggregation query + since the data only updates during the extraction cronjob. Args iocs_qs (QuerySet): Filtered IOC queryset from get_queryset; request (Request): The API request object; feed_params (FeedRequestParams): Validated parameter object - Returns: A values-grouped queryset with annotated metrics and honeypot arrays. + Returns: A list of dicts with aggregated metrics and honeypot arrays per ASN. """ + + # Build reliable cache key from query params + sorted_params = sorted(request.query_params.lists()) + params_string = urllib.parse.urlencode(sorted_params, doseq=True) + param_hash = hashlib.sha256(params_string.encode("utf-8")).hexdigest() + + # To prevent per-worker continuous RAM bloat, use the shared DB-backed cache + # instead of the default LocMemCache, since the JSON response size can be large. + # The extraction pipeline invalidates this cache by bumping the version counter. + shared_cache = caches["django-q"] + version = shared_cache.get("asn_feeds_version", 1) + cache_key = f"asn_feeds_v{version}_{param_hash}" + + cached_result = shared_cache.get(cache_key) + if cached_result is not None: + return cached_result + asn_filter = request.query_params.get("asn") if asn_filter: iocs_qs = iocs_qs.filter(autonomous_system__asn=asn_filter) @@ -480,24 +500,25 @@ def asn_aggregated_queryset(iocs_qs, request, feed_params): first_seen=Min("first_seen"), last_seen=Max("last_seen"), ) - .order_by(ordering) ) + numeric_agg = numeric_agg.order_by(ordering) + # Honeypot names still require a lightweight aggregation because + # they depend on the active flag which can change independently. honeypot_agg = ( iocs_qs.exclude(autonomous_system__isnull=True) - .filter(general_honeypot__active=True) + .filter(honeypots__active=True) .values(asn=F("autonomous_system__asn")) .annotate( - honeypots=ArrayAgg( - "general_honeypot__name", + honeypot_names=ArrayAgg( + "honeypots__name", distinct=True, ) ) ) - hp_lookup = {row["asn"]: row["honeypots"] or [] for row in honeypot_agg} + hp_lookup = {row["asn"]: row["honeypot_names"] or [] for row in honeypot_agg} - # merging numeric aggregate with honeypot names for each asn result = [] for row in numeric_agg: asn = row["asn"] @@ -505,6 +526,9 @@ def asn_aggregated_queryset(iocs_qs, request, feed_params): row_dict["honeypots"] = sorted(hp_lookup.get(asn, [])) result.append(row_dict) + # Set cache with a 60-minute timeout (max extraction interval length) to prevent memory bloat + shared_cache.set(cache_key, result, timeout=3600) + return result diff --git a/configuration/gunicorn/config.py b/configuration/gunicorn/config.py index 6d001381..d5765f82 100644 --- a/configuration/gunicorn/config.py +++ b/configuration/gunicorn/config.py @@ -1,10 +1,10 @@ -import multiprocessing +import os # Server socket bind = "unix:/run/gunicorn/main.sock" # Worker processes -workers = 2 * multiprocessing.cpu_count() + 1 +workers = 2 * len(os.sched_getaffinity(0)) + 1 max_requests = 1000 max_requests_jitter = 50 diff --git a/docker/Dockerfile b/docker/Dockerfile index 36775739..8e5839d3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -37,22 +37,30 @@ ENV UV_PROJECT_ENVIRONMENT=/usr/local WORKDIR $APP_ROOT -# Install runtime dependencies -# - libgomp1 is required for model training -# - curl is used for healthcheck -RUN apt-get update && apt-get install -y --no-install-recommends \ - libgomp1 curl gosu \ +# Layer 1: stable runtime OS deps — cached across pyproject.toml/uv.lock changes. +# libgomp1: model training; curl: healthcheck; gosu: entrypoint privilege drop +# libpq5: runtime shared library required by the psycopg[c] C extension +RUN apt-get update \ + && apt-get install -y --no-install-recommends libgomp1 curl gosu libpq5 \ && rm -rf /var/lib/apt/lists/* -# Install python packages +# Layer 2: Python packages — only re-runs when pyproject.toml/uv.lock change. +# Build-only deps (gcc, python3-dev, libpq-dev) compile the psycopg[c] C +# extension and are purged in the same layer to keep the final image lean. COPY pyproject.toml uv.lock ./ -RUN uv sync --no-dev --locked +RUN apt-get update \ + && apt-get install -y --no-install-recommends gcc python3-dev libpq-dev \ + && uv sync --no-dev --locked \ + && uv cache clean \ + && apt-get purge -y gcc python3-dev libpq-dev \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* # Copy files COPY . $APP_ROOT COPY --from=frontend-build /app/build /var/www/reactapp -# separation is required to avoid to re-execute os installation in case of change of python requirements +# Set up log directories, fix permissions, and remove frontend source (served from /var/www/reactapp) RUN mkdir -p ${LOG_PATH}/django ${LOG_PATH}/gunicorn \ && touch ${LOG_PATH}/django/api.log ${LOG_PATH}/django/api_errors.log \ && touch ${LOG_PATH}/django/greedybear.log ${LOG_PATH}/django/greedybear_errors.log \ @@ -60,7 +68,6 @@ RUN mkdir -p ${LOG_PATH}/django ${LOG_PATH}/gunicorn \ && touch ${LOG_PATH}/django/django_errors.log ${LOG_PATH}/django/elasticsearch.log \ && touch ${LOG_PATH}/django/authentication.log ${LOG_PATH}/django/authentication_errors.log \ && mkdir -p ${APP_ROOT}/mlmodels \ - && usermod -u 2000 www-data \ && chown -R www-data:www-data ${LOG_PATH} /opt/deploy/ ${APP_ROOT}/mlmodels/ \ && rm -rf frontend/ diff --git a/docker/default.yml b/docker/default.yml index c7c2600d..699d4abf 100644 --- a/docker/default.yml +++ b/docker/default.yml @@ -71,6 +71,8 @@ services: container_name: greedybear_qcluster restart: unless-stopped stop_grace_period: 3m + entrypoint: + - ./docker/entrypoint_qcluster.sh command: sh -c "python manage.py setup_schedules && exec python manage.py qcluster" volumes: - generic_logs:/var/log/greedybear @@ -82,7 +84,6 @@ services: condition: service_healthy app: condition: service_healthy - user: "2000:82" healthcheck: disable: true diff --git a/docker/entrypoint_gunicorn.sh b/docker/entrypoint_gunicorn.sh index 1d71e4c4..3de924bd 100755 --- a/docker/entrypoint_gunicorn.sh +++ b/docker/entrypoint_gunicorn.sh @@ -20,8 +20,9 @@ python manage.py collectstatic --noinput --clear --verbosity 0 mkdir -p /var/log/greedybear/gunicorn mkdir -p /run/gunicorn -# Fix log file ownership (manage.py commands above run as root and may create new log files) -chown -R 2000:82 /var/log/greedybear /run/gunicorn +# Fix log file ownership (manage.py commands above run as root +# and may create new log files owned by root instead of www-data) +chown -R www-data:www-data /var/log/greedybear /run/gunicorn # Obtain the current GreedyBear version number GREEDYBEAR_VERSION=$(uv version --short) diff --git a/docker/entrypoint_qcluster.sh b/docker/entrypoint_qcluster.sh new file mode 100755 index 00000000..965a9421 --- /dev/null +++ b/docker/entrypoint_qcluster.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Fix mlmodels ownership (volumes may retain files owned by a previous UID) +chown -R www-data:www-data /opt/deploy/greedybear/mlmodels + +if [ "$DJANGO_TEST_SERVER" = "True" ]; then + # Dev mode: run as root (needed for hot-reload on volume-mounted source) + exec "$@" +else + # Production mode: drop privileges to www-data before starting qcluster + exec gosu www-data "$@" +fi diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9bfe1abb..f6a5744a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,7 +23,7 @@ "react-use": "^17.6.0", "reactstrap": "^9.2.3", "recharts": "^2.15.4", - "sass": "^1.98.0", + "sass": "^1.99.0", "zustand": "^4.5.7" }, "devDependencies": { @@ -31,14 +31,14 @@ "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.6.1", "@vitejs/plugin-react": "^6.0.1", - "@vitest/coverage-v8": "^4.1.2", + "@vitest/coverage-v8": "^4.1.3", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^4.6.0", - "jsdom": "^29.0.1", + "jsdom": "^29.0.2", "prettier": "^3.8.1", "stylelint": "^17.6.0", "vite": "^8.0.3", @@ -52,41 +52,30 @@ "dev": true }, "node_modules/@asamuzakjp/css-color": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", - "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.8.tgz", + "integrity": "sha512-OISPR9c2uPo23rUdvfEQiLPjoMLOpEeLNnP5iGkxr6tDDxJd3NjD+6fxY0mdaMbIPUjFGL4HFOJqLvow5q4aqQ==", "dev": true, "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.6" + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", - "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.8.tgz", + "integrity": "sha512-erMO6FgtM02dC24NGm0xufMzWz5OF0wXKR7BpvGD973bq/GbmR8/DbxNZbj0YevQ5hlToJaWSVK/G9/NDgGEVw==", "dev": true, "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.7" + "is-potential-custom-element-name": "^1.0.1" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -105,15 +94,6 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@asamuzakjp/dom-selector/node_modules/mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", @@ -1962,13 +1942,13 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz", - "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.3.tgz", + "integrity": "sha512-/MBdrkA8t6hbdCWFKs09dPik774xvs4Z6L4bycdCxYNLHM8oZuRyosumQMG19LUlBsB6GeVpL1q4kFFazvyKGA==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.2", + "@vitest/utils": "4.1.3", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -1982,8 +1962,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.2", - "vitest": "4.1.2" + "@vitest/browser": "4.1.3", + "vitest": "4.1.3" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1992,15 +1972,15 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", - "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.3.tgz", + "integrity": "sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ==", "dev": true, "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/spy": "4.1.3", + "@vitest/utils": "4.1.3", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2009,12 +1989,12 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", - "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.3.tgz", + "integrity": "sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw==", "dev": true, "dependencies": { - "@vitest/spy": "4.1.2", + "@vitest/spy": "4.1.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2035,9 +2015,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", - "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.3.tgz", + "integrity": "sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg==", "dev": true, "dependencies": { "tinyrainbow": "^3.1.0" @@ -2047,12 +2027,12 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", - "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.3.tgz", + "integrity": "sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA==", "dev": true, "dependencies": { - "@vitest/utils": "4.1.2", + "@vitest/utils": "4.1.3", "pathe": "^2.0.3" }, "funding": { @@ -2060,13 +2040,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", - "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.3.tgz", + "integrity": "sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ==", "dev": true, "dependencies": { - "@vitest/pretty-format": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/pretty-format": "4.1.3", + "@vitest/utils": "4.1.3", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2075,21 +2055,21 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", - "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.3.tgz", + "integrity": "sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw==", "dev": true, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", - "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.3.tgz", + "integrity": "sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw==", "dev": true, "dependencies": { - "@vitest/pretty-format": "4.1.2", + "@vitest/pretty-format": "4.1.3", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -5436,13 +5416,13 @@ } }, "node_modules/jsdom": { - "version": "29.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", - "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", + "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "dependencies": { - "@asamuzakjp/css-color": "^5.0.1", - "@asamuzakjp/dom-selector": "^7.0.3", + "@asamuzakjp/css-color": "^5.1.5", + "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", @@ -7438,9 +7418,9 @@ } }, "node_modules/sass": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", - "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", @@ -8845,18 +8825,18 @@ } }, "node_modules/vitest": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", - "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", - "dev": true, - "dependencies": { - "@vitest/expect": "4.1.2", - "@vitest/mocker": "4.1.2", - "@vitest/pretty-format": "4.1.2", - "@vitest/runner": "4.1.2", - "@vitest/snapshot": "4.1.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.3.tgz", + "integrity": "sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw==", + "dev": true, + "dependencies": { + "@vitest/expect": "4.1.3", + "@vitest/mocker": "4.1.3", + "@vitest/pretty-format": "4.1.3", + "@vitest/runner": "4.1.3", + "@vitest/snapshot": "4.1.3", + "@vitest/spy": "4.1.3", + "@vitest/utils": "4.1.3", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -8884,10 +8864,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.2", - "@vitest/browser-preview": "4.1.2", - "@vitest/browser-webdriverio": "4.1.2", - "@vitest/ui": "4.1.2", + "@vitest/browser-playwright": "4.1.3", + "@vitest/browser-preview": "4.1.3", + "@vitest/browser-webdriverio": "4.1.3", + "@vitest/coverage-istanbul": "4.1.3", + "@vitest/coverage-v8": "4.1.3", + "@vitest/ui": "4.1.3", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -8911,6 +8893,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -9199,37 +9187,27 @@ "dev": true }, "@asamuzakjp/css-color": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", - "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.8.tgz", + "integrity": "sha512-OISPR9c2uPo23rUdvfEQiLPjoMLOpEeLNnP5iGkxr6tDDxJd3NjD+6fxY0mdaMbIPUjFGL4HFOJqLvow5q4aqQ==", "dev": true, "requires": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.6" - }, - "dependencies": { - "lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true - } + "@csstools/css-tokenizer": "^4.0.0" } }, "@asamuzakjp/dom-selector": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", - "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.8.tgz", + "integrity": "sha512-erMO6FgtM02dC24NGm0xufMzWz5OF0wXKR7BpvGD973bq/GbmR8/DbxNZbj0YevQ5hlToJaWSVK/G9/NDgGEVw==", "dev": true, "requires": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.7" + "is-potential-custom-element-name": "^1.0.1" }, "dependencies": { "css-tree": { @@ -9242,12 +9220,6 @@ "source-map-js": "^1.2.1" } }, - "lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true - }, "mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", @@ -10394,13 +10366,13 @@ } }, "@vitest/coverage-v8": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz", - "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.3.tgz", + "integrity": "sha512-/MBdrkA8t6hbdCWFKs09dPik774xvs4Z6L4bycdCxYNLHM8oZuRyosumQMG19LUlBsB6GeVpL1q4kFFazvyKGA==", "dev": true, "requires": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.2", + "@vitest/utils": "4.1.3", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -10412,74 +10384,74 @@ } }, "@vitest/expect": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", - "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.3.tgz", + "integrity": "sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ==", "dev": true, "requires": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/spy": "4.1.3", + "@vitest/utils": "4.1.3", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "@vitest/mocker": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", - "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.3.tgz", + "integrity": "sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw==", "dev": true, "requires": { - "@vitest/spy": "4.1.2", + "@vitest/spy": "4.1.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" } }, "@vitest/pretty-format": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", - "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.3.tgz", + "integrity": "sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg==", "dev": true, "requires": { "tinyrainbow": "^3.1.0" } }, "@vitest/runner": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", - "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.3.tgz", + "integrity": "sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA==", "dev": true, "requires": { - "@vitest/utils": "4.1.2", + "@vitest/utils": "4.1.3", "pathe": "^2.0.3" } }, "@vitest/snapshot": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", - "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.3.tgz", + "integrity": "sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ==", "dev": true, "requires": { - "@vitest/pretty-format": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/pretty-format": "4.1.3", + "@vitest/utils": "4.1.3", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "@vitest/spy": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", - "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.3.tgz", + "integrity": "sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw==", "dev": true }, "@vitest/utils": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", - "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.3.tgz", + "integrity": "sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw==", "dev": true, "requires": { - "@vitest/pretty-format": "4.1.2", + "@vitest/pretty-format": "4.1.3", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -12883,13 +12855,13 @@ } }, "jsdom": { - "version": "29.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", - "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", + "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "requires": { - "@asamuzakjp/css-color": "^5.0.1", - "@asamuzakjp/dom-selector": "^7.0.3", + "@asamuzakjp/css-color": "^5.1.5", + "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", @@ -14205,9 +14177,9 @@ } }, "sass": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", - "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "requires": { "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", @@ -15148,18 +15120,18 @@ } }, "vitest": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", - "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", - "dev": true, - "requires": { - "@vitest/expect": "4.1.2", - "@vitest/mocker": "4.1.2", - "@vitest/pretty-format": "4.1.2", - "@vitest/runner": "4.1.2", - "@vitest/snapshot": "4.1.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.3.tgz", + "integrity": "sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw==", + "dev": true, + "requires": { + "@vitest/expect": "4.1.3", + "@vitest/mocker": "4.1.3", + "@vitest/pretty-format": "4.1.3", + "@vitest/runner": "4.1.3", + "@vitest/snapshot": "4.1.3", + "@vitest/spy": "4.1.3", + "@vitest/utils": "4.1.3", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", diff --git a/frontend/package.json b/frontend/package.json index 67a24135..8d1f84d2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,7 @@ "react-use": "^17.6.0", "reactstrap": "^9.2.3", "recharts": "^2.15.4", - "sass": "^1.98.0", + "sass": "^1.99.0", "zustand": "^4.5.7" }, "scripts": { @@ -50,14 +50,14 @@ "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.6.1", "@vitejs/plugin-react": "^6.0.1", - "@vitest/coverage-v8": "^4.1.2", + "@vitest/coverage-v8": "^4.1.3", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^4.6.0", - "jsdom": "^29.0.1", + "jsdom": "^29.0.2", "prettier": "^3.8.1", "stylelint": "^17.6.0", "vite": "^8.0.3", diff --git a/frontend/src/components/auth/Login.jsx b/frontend/src/components/auth/Login.jsx index 6fd44ac1..b63922fc 100644 --- a/frontend/src/components/auth/Login.jsx +++ b/frontend/src/components/auth/Login.jsx @@ -78,7 +78,7 @@ function Login() { id="access-info" className="col-12 px-1 text-center" > -
+
 New users
diff --git a/frontend/src/components/auth/api.js b/frontend/src/components/auth/api.js index b348b22f..7dacae04 100644 --- a/frontend/src/components/auth/api.js +++ b/frontend/src/components/auth/api.js @@ -51,7 +51,7 @@ export async function requestPasswordReset(body) { return resp; } catch (err) { addToast("Failed to send email!", err.parsedMsg, "danger", true); - return null; + return Promise.reject(err); } } diff --git a/frontend/src/components/dashboard/utils/charts.jsx b/frontend/src/components/dashboard/utils/charts.jsx index 6091cddd..ed49d705 100644 --- a/frontend/src/components/dashboard/utils/charts.jsx +++ b/frontend/src/components/dashboard/utils/charts.jsx @@ -139,7 +139,7 @@ export const AttackOriginCountriesChart = React.memo(() => { const { range } = useTimePickerStore(); const { - rawData: data, + normalizedData: data, loading, error, fetchData, diff --git a/frontend/src/components/feeds/tableColumns.jsx b/frontend/src/components/feeds/tableColumns.jsx index a7de0078..3eb7ce21 100644 --- a/frontend/src/components/feeds/tableColumns.jsx +++ b/frontend/src/components/feeds/tableColumns.jsx @@ -40,7 +40,7 @@ const feedsTableColumns = [ Array.isArray(value) ? (