From 314702e4d60ccbf8435350a7da19b300f1513ed1 Mon Sep 17 00:00:00 2001 From: 0verwrite <31691645+overwrite00@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:53:39 +0100 Subject: [PATCH] fix: resolve dependency alerts and security regressions --- .github/dependabot.yml | 5 +- .github/workflows/codeql.yml | 11 +- .github/workflows/project-automation.yml | 2 +- .github/workflows/tests.yml | 12 +- CHANGELOG.md | 46 +++++ README.md | 6 +- backend/api/routes/reputation.py | 37 ++-- backend/core/analysis/nlp_classifier.py | 57 ++---- backend/core/analysis/url_analyzer.py | 14 +- backend/core/intel/fetch.py | 6 +- backend/core/reputation/connectors.py | 116 ++++++++--- backend/nlp_retrain_tabular_model.py | 7 + .../nlp_model_tabular_v0.15.1.pkl.hmac | 1 - backend/tests/test_security_regressions.py | 137 +++++++++++++ backend/utils/config.py | 4 +- docs/INSTALLATION.md | 6 +- frontend/package-lock.json | 182 +++++++++--------- frontend/package.json | 6 +- 18 files changed, 446 insertions(+), 209 deletions(-) delete mode 100644 backend/nlp_training/nlp_model_tabular_v0.15.1.pkl.hmac create mode 100644 backend/tests/test_security_regressions.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1609b60..f44a1a2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,8 @@ version: 2 -# Dependabot auto-creates PRs against `develop` (not `main`). -# `main` is reserved for releases and is protected — all updates flow through develop. +# Gli aggiornamenti di versione puntano a `develop`. GitHub forza invece le PR di +# sicurezza Dependabot sul branch predefinito (`main`): vanno replicate su `develop`, +# validate e promosse con la normale PR develop -> main; la PR bot viene poi chiusa. # Configuration strategy: # - production deps: minor/patch updates auto-applied (fasttrack) # - dev deps: only minor/patch (no majors — prevents breaking CI changes) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8b478b7..b8738c4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,16 +29,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4 with: languages: ${{ matrix.language }} - name: Set up Python (for Python analysis) if: matrix.language == 'python' - uses: actions/setup-python@v7 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7 with: python-version: '3.13' cache: 'pip' @@ -49,7 +49,7 @@ jobs: - name: Set up Node.js (for JavaScript analysis) if: matrix.language == 'javascript-typescript' - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: '20' cache: 'npm' @@ -60,7 +60,6 @@ jobs: run: cd frontend && npm ci - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4 with: category: "/language:${{matrix.language}}" - upload: ${{ github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml index 965b1e3..62ea94a 100644 --- a/.github/workflows/project-automation.yml +++ b/.github/workflows/project-automation.yml @@ -22,7 +22,7 @@ jobs: steps: - name: "Checkout repository (for file detection)" - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a903032..98561cd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,10 +29,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Set up Python 3.13 - uses: actions/setup-python@v7 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7 with: python-version: '3.13' cache: 'pip' @@ -56,7 +56,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: pytest-results path: backend/pytest-results.xml @@ -64,7 +64,7 @@ jobs: - name: Upload coverage report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: coverage-report path: backend/coverage.xml @@ -86,10 +86,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Set up Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: '20' cache: 'npm' diff --git a/CHANGELOG.md b/CHANGELOG.md index c527534..f34c848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,52 @@ Features are ordered by implementation priority. --- +## [0.17.3] — 2026-10-25 + +### Security + +- Frontend dependency fixes: `source-map-js` 1.2.1 → 1.2.2, Vite 8.3.1 → + 8.3.5, and `follow-redirects` 1.16.0 → 1.16.1 resolve all nine open + Dependabot alerts (denial of service, development-server command/path issues, + and cross-origin credential-header disclosure). +- Updated transitive development dependency Zod 4.3.6 → 4.6.7, resolving an + additional high-severity memory-exhaustion advisory detected by `npm audit` + before GitHub opened a repository alert. +- The URLScan diagnostic endpoint now requires a loopback client or the admin + token, is limited to two requests per minute, and performs its blocking HTTP + diagnostics in a worker thread instead of blocking FastAPI's event loop. +- Redirect-chain analysis now validates the initial URL and every redirect hop, + blocks loopback/private/link-local/reserved destinations, limits redirect + depth, and removes credentials and query tokens from its result detail. +- Replaced the ineffective model-file HMAC (whose public, filename-derived key + provided no authenticity) with a release-anchored SHA-256 digest checked + before `pickle.loads`. Retraining now prints the digest that must be reviewed. +- Pinned every GitHub Action to a reviewed full commit SHA. CodeQL now uploads + SARIF on pull requests instead of reporting a green check without publishing + its findings. + +### Fixed + +- Reputation hostname extraction now uses `urlparse().hostname` and an exact + `www.` prefix removal. Hosts such as `web.example.com`, URLs with userinfo or + ports, and IPv6 literals are no longer mangled before URLScan, redirect-chain, + crt.sh, CIRCL or SecurityTrails checks. +- Reputation and URL-analysis logs no longer record full URLs, avoiding leakage + of userinfo, query tokens and fragments into application logs. +- Corrected the Dependabot configuration comments: security updates always + target GitHub's default branch, so they must be reproduced and validated on + permanent `develop` before promotion to `main`. + +### Tests + +- Added regression coverage for hostname normalization and trusted-model digest + validation. +- Backend suite: 213 passed, 1 skipped without the optional RTFDE package. +- `npm audit` and `npm audit --omit=dev` report zero vulnerabilities; the Python + requirements report no known vulnerabilities with `pip-audit`. + +--- + ## [0.17.2] — 2026-10-02 ### Fixed diff --git a/README.md b/README.md index 2886dc8..cd35b41 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ Email (.eml / .msg / plain text) ## 🔧 Version +**v0.17.3** — 🔒 Security and correctness release: resolves the open frontend dependency advisories for Vite, `source-map-js`, `follow-redirects` and Zod; protects the URLScan diagnostic endpoint; blocks SSRF in redirect-chain analysis; anchors the bundled NLP model to a reviewed SHA-256 digest; fixes hostname extraction in reputation checks; and restores CodeQL result uploads on pull requests while pinning GitHub Actions to full commit SHAs. The suite contains 214 tests (213 pass locally and 1 optional RTFDE test is skipped). + **v0.17.2** — 🛠️ Bugfix release for Outlook `.msg` files: recipients, SPF/DKIM/DMARC results, `Message-ID`, the `Received` chain and the other transport-header fields were silently dropped, so header scoring ran without authentication data (re-analyze older `.msg` analyses to recompute them); RTF-only `.msg` bodies (Outlook 97-2003) are now recoverable with the optional RTFDE package. Also: SQLAlchemy 2.1 with the `asyncio` extra, a high-severity dev-dependency advisory (`brace-expansion`) resolved, other dependency updates, and a repaired project-automation workflow. All 199 tests passing ✅ in CI (the one end-to-end test that needs the optional RTFDE package is skipped where it is not installed), zero regressions. **v0.17.1** — 🐛 Follow-up to the Threat Intelligence release: fixed CERT-AGID bulletins not loading on first visit, added example placeholders and a collapsible mini-guide to the known-campaign form, and added a "Ri-analizza" (Re-analyze) button so an already-analyzed email can pick up a known campaign created or edited afterward. Documented the whole Threat Intelligence panel in `docs/USAGE.md`. @@ -216,5 +218,5 @@ Distributed with MIT License · [View License](LICENSE) --- -*Last updated: 2026-09-15* -*← [Contributing](./CONTRIBUTING.md) | [Docs →](./docs/)* \ No newline at end of file +*Last updated: 2026-10-25* +*← [Contributing](./CONTRIBUTING.md) | [Docs →](./docs/)* diff --git a/backend/api/routes/reputation.py b/backend/api/routes/reputation.py index 5b54187..85d58cc 100644 --- a/backend/api/routes/reputation.py +++ b/backend/api/routes/reputation.py @@ -24,7 +24,7 @@ import asyncio import ipaddress import logging -from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks +from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks, Request from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm.attributes import flag_modified @@ -35,7 +35,10 @@ run_slow_checks, finalize_fast_only, ReputationSummary, + normalize_url_host, ) +from core.intel.auth import require_trusted_client +from core.rate_limiting import limiter from dataclasses import asdict import json @@ -179,8 +182,7 @@ def add_url(raw: str, is_suspicious: bool = False) -> None: # Estrai hostname dall'URL try: - from urllib.parse import urlparse - hostname = urlparse(url).netloc.split(":")[0].removeprefix("www.") + hostname = normalize_url_host(url) except Exception: hostname = "" @@ -193,7 +195,7 @@ def add_url(raw: str, is_suspicious: bool = False) -> None: # Altrimenti, escludi URL da CDN/trusted domains if _is_trusted_cdn(hostname): - _logger.debug(f"[FILTRO] URL {url} da CDN legittima - escluso") + _logger.debug("[FILTRO] Host %s da CDN legittima - escluso", hostname) return seen_urls.add(url) @@ -211,8 +213,7 @@ def add_domain(raw: str) -> None: if not raw: return try: - from urllib.parse import urlparse - hostname = urlparse(raw).netloc.split(":")[0].removeprefix("www.") + hostname = normalize_url_host(raw) # Scarta IP diretti if hostname and not (hostname.count(".") < 1 or hostname.replace(".", "").isdigit()): if hostname not in seen_domains and not _is_trusted_cdn(hostname): @@ -370,13 +371,12 @@ def add_url_if_worth_checking(u: dict) -> None: # Estrai hostname e controlla se trusted CDN (escludi sempre) try: - from urllib.parse import urlparse - hostname = urlparse(url_str).netloc.split(":")[0].removeprefix("www.") + hostname = normalize_url_host(url_str) except Exception: hostname = "" if _is_trusted_cdn(hostname): - _logger.debug(f"[FILTRO SLOW] URL {url_str} da trusted CDN - esclusa") + _logger.debug("[FILTRO SLOW] Host %s da trusted CDN - escluso", hostname) return # Determina se è sospetta (per logging) @@ -394,9 +394,9 @@ def add_url_if_worth_checking(u: dict) -> None: seen_urls.add(url_str) urls.append(url_str) if is_suspicious: - _logger.debug(f"[FILTRO SLOW] URL {url_str} (sospetta, risk_score={u.get('risk_score', 0)}) - inclusa") + _logger.debug("[FILTRO SLOW] Host %s sospetto (risk_score=%s) incluso", hostname, u.get("risk_score", 0)) else: - _logger.debug(f"[FILTRO SLOW] URL {url_str} (normale, non da CDN) - inclusa per coverage") + _logger.debug("[FILTRO SLOW] Host %s non-CDN incluso per coverage", hostname) # ───────────────────────────────────────────────────────────────────────── # IP: Sender IP + primi 2 hop (mittente -> intermediari prossimi) @@ -451,8 +451,7 @@ def add_url_if_worth_checking(u: dict) -> None: url_str = u.get("original_url") or u.get("url", "") if url_str: try: - from urllib.parse import urlparse - hostname = urlparse(url_str).netloc.split(":")[0].removeprefix("www.") + hostname = normalize_url_host(url_str) is_suspicious = ( u.get("is_ip_address") or u.get("is_ip") or u.get("is_shortener") or u.get("is_new_domain") or @@ -667,8 +666,7 @@ def to_result(r: dict) -> ReputationResult: # Phase 2C: Health Check Endpoint per URLScan.io (v0.14.3+) # --------------------------------------------------------------------------- -@router.get("/test-urlscan") -async def test_urlscan_health(): +def _test_urlscan_health_sync() -> dict: """ Testa la connessione a URLScan.io e verifica la configurazione dell'API key. Utile per diagnosticare problemi di autenticazione o rate limiting. @@ -736,4 +734,11 @@ async def test_urlscan_health(): else: result["suggestions"].append("URLSCAN_API_KEY non configurato in .env. Registrati su https://urlscan.io/user/signup e configura la chiave per aumentare il limite a 1000 req/giorno") - return result \ No newline at end of file + return result + + +@router.get("/test-urlscan", dependencies=[Depends(require_trusted_client)]) +@limiter.limit("2/minute") +async def test_urlscan_health(request: Request): + """Esegue la diagnostica URLScan fuori dall'event loop per client fidati.""" + return await asyncio.to_thread(_test_urlscan_health_sync) diff --git a/backend/core/analysis/nlp_classifier.py b/backend/core/analysis/nlp_classifier.py index e25f332..2fbadca 100644 --- a/backend/core/analysis/nlp_classifier.py +++ b/backend/core/analysis/nlp_classifier.py @@ -19,7 +19,6 @@ import threading import pickle import hashlib -import hmac from pathlib import Path from dataclasses import dataclass, field from typing import Optional @@ -357,44 +356,18 @@ class NLPResult: _tabular_model = None _tabular_lock = threading.Lock() +_TABULAR_MODEL_SHA256 = "ee129c78d7ee9347087bf20b24cdd4e58c34f705e87435f0e56465951276304e" -def _verify_and_load_pickle(pkl_path: Path): - """Carica il pickle solo se l'HMAC-SHA256 è valido (protezione anti-tampering). - - Se il file HMAC non esiste, crea uno nuovo (init-time). Questo consente - il deployment iniziale senza dover pre-generare gli HMAC, ma da quel momento - in poi il pickle è protetto. - - Se l'HMAC non corrisponde, rifiuta e lancia un'eccezione. - """ - hmac_path = pkl_path.with_suffix('.pkl.hmac') - - # Leggi i dati del pickle +def _verify_and_load_pickle(pkl_path: Path, expected_sha256: str): + """Carica un pickle solo se coincide con il digest della release fidata.""" pkl_data = pkl_path.read_bytes() - - # Calcola HMAC-SHA256 dei dati - # Nota: usiamo una chiave derivata dal path come "salt" — non è una vera chiave - # segreta (il filesystem può essere compromesso interamente), ma aggiunge un - # ulteriore livello di protezione contro modifiche accidentali o triviali - _secret = f"emlyzer-pickle-{pkl_path.name}".encode() - computed_hmac = hmac.new(_secret, pkl_data, hashlib.sha256).hexdigest() - - # Se il file HMAC esiste, verifica - if hmac_path.exists(): - stored_hmac = hmac_path.read_text().strip() - if not hmac.compare_digest(computed_hmac, stored_hmac): - raise RuntimeError( - f"Pickle integrity check FAILED for {pkl_path.name}: " - f"HMAC mismatch. File may have been tampered with or corrupted. " - f"Refusing to load." - ) - else: - # Crea il file HMAC la prima volta (init-time) - hmac_path.write_text(computed_hmac) - logger.info(f"Created HMAC file for {pkl_path.name} (init-time)") - - # HMAC valido, carica il pickle + computed_sha256 = hashlib.sha256(pkl_data).hexdigest() + if computed_sha256 != expected_sha256: + raise RuntimeError( + f"Pickle integrity check FAILED for {pkl_path.name}: " + "SHA-256 mismatch. Refusing to deserialize an untrusted model." + ) return pickle.loads(pkl_data) @@ -402,10 +375,9 @@ def _load_tabular_model(): """ Carica il modello Random Forest serializzato (v0.15.1). - SECURITY NOTE: The pickle file is generated internally by nlp_retrain_tabular_model.py - during the training process. It is NOT loaded from untrusted sources, making it safe - to deserialize. The model contains only a trained sklearn RandomForestClassifier and - StandardScaler with no arbitrary code execution risk. + SECURITY NOTE: pickle can execute code while deserializing. The bundled model is + loaded only after matching the SHA-256 digest anchored in this module. When the + model is intentionally retrained, the digest must be reviewed and updated together. """ global _tabular_model @@ -423,10 +395,7 @@ def _load_tabular_model(): logger.debug(f"Tabular model not found at {model_path}") return None - # Load with HMAC-SHA256 integrity verification (prevents tampering) - # Model is generated internally from training, but filesystem can be compromised - # HMAC protects against accidental or malicious file modification - model_data = _verify_and_load_pickle(model_path) + model_data = _verify_and_load_pickle(model_path, _TABULAR_MODEL_SHA256) _tabular_model = model_data logger.info("Tabular NLP model (Random Forest v0.15.1) loaded successfully") diff --git a/backend/core/analysis/url_analyzer.py b/backend/core/analysis/url_analyzer.py index 90bb70a..99b1b4b 100644 --- a/backend/core/analysis/url_analyzer.py +++ b/backend/core/analysis/url_analyzer.py @@ -26,6 +26,14 @@ _logger = logging.getLogger(__name__) +def _safe_log_host(url: str) -> str: + """Restituisce solo l'host per i log, senza credenziali, query o frammenti.""" + try: + return urllib.parse.urlparse(url).hostname or "" + except (TypeError, ValueError): + return "" + + # URL shortener noti (stesso set usato in body_analyzer). # Rilevamento shortener è indicatore di URL offuscato → rischio MEDIUM. URL_SHORTENER_DOMAINS = { @@ -311,7 +319,7 @@ def _analyze_single_url( "description": t("url.malicious_cdn", cdn=cdn_check["cdn"]), "evidence": f"CDN: {cdn_check['cdn']} — {cdn_check['reason']}", }) - _logger.warning("[URL] Malicious CDN detected: %s in URL: %s", cdn_check["cdn"], url) + _logger.warning("[URL] Malicious CDN detected: %s on host: %s", cdn_check["cdn"], clean_host) # DNS lookup con timeout ip, dns_err = _resolve_ip(clean_host) @@ -457,7 +465,7 @@ def analyze_urls(urls: list[str], do_whois: bool = True) -> URLAnalysisResult: analyses.append(future.result()) except Exception as e: url = future_map[future] - _logger.warning("[URL] Failed to analyze URL %s: %s", url, str(e)) + _logger.warning("[URL] Failed to analyze host %s: %s", _safe_log_host(url), type(e).__name__) except concurrent.futures.TimeoutError: # Raccoglie i risultati già disponibili entro il timeout di batch _logger.warning("[URL] Batch analysis timeout (%ds) - collecting partial results", URL_BATCH_TIMEOUT) @@ -467,7 +475,7 @@ def analyze_urls(urls: list[str], do_whois: bool = True) -> URLAnalysisResult: analyses.append(future.result()) except Exception as e: url = future_map[future] - _logger.warning("[URL] Failed to analyze URL %s after timeout: %s", url, str(e)) + _logger.warning("[URL] Failed to analyze host %s after timeout: %s", _safe_log_host(url), type(e).__name__) finally: executor.shutdown(wait=False, cancel_futures=True) diff --git a/backend/core/intel/fetch.py b/backend/core/intel/fetch.py index 5f7baed..3565406 100644 --- a/backend/core/intel/fetch.py +++ b/backend/core/intel/fetch.py @@ -77,7 +77,7 @@ def _is_unsafe_host(hostname: str) -> bool: return False -def _check_url_safety(url: str, *, previous_scheme: str | None = None) -> None: +def check_url_safety(url: str, *, previous_scheme: str | None = None) -> None: parsed = urllib.parse.urlparse(url) if parsed.scheme != "https": if previous_scheme == "https" and parsed.scheme == "http": @@ -108,7 +108,7 @@ def fetch_bytes( deadline = deadline if deadline is not None else settings.INTEL_DOWNLOAD_DEADLINE started = time.monotonic() - _check_url_safety(url) + check_url_safety(url) try: resp = requests.get( @@ -135,7 +135,7 @@ def fetch_bytes( if not location: break next_url = urllib.parse.urljoin(resp.url, location) - _check_url_safety(next_url, previous_scheme=current_scheme) + check_url_safety(next_url, previous_scheme=current_scheme) current_scheme = urllib.parse.urlparse(next_url).scheme try: resp = requests.get( diff --git a/backend/core/reputation/connectors.py b/backend/core/reputation/connectors.py index 6800119..5c30591 100644 --- a/backend/core/reputation/connectors.py +++ b/backend/core/reputation/connectors.py @@ -23,6 +23,7 @@ from dataclasses import dataclass, field from utils.config import settings from core.intel import feeds as intel_feeds +from core.intel.fetch import FetchError, check_url_safety logger = logging.getLogger(__name__) @@ -31,6 +32,33 @@ REQUEST_TIMEOUT_INFO = 8 # servizi informativi (crt.sh, redirect chain) — crt.sh può essere molto lento REQUEST_TIMEOUT_PULSEDIVE = 15 # Pulsedive può essere lento (free tier: 10 req/day, rate limit aggressivo) + +def normalize_url_host(url: str) -> str: + """Estrae un hostname normalizzato senza alterare prefissi validi. + + ``urlparse().hostname`` gestisce correttamente porte, userinfo e IPv6; + ``removeprefix`` rimuove solo l'etichetta ``www.`` completa. + """ + try: + return (urllib.parse.urlparse(url).hostname or "").lower().removeprefix("www.") + except (TypeError, ValueError): + return "" + + +def _redacted_url(url: str) -> str: + """URL adatto a log/UI: niente credenziali, query o frammenti.""" + try: + parsed = urllib.parse.urlparse(url) + host = parsed.hostname or "" + if not host: + return "" + display_host = f"[{host}]" if ":" in host else host + if parsed.port is not None: + display_host = f"{display_host}:{parsed.port}" + return urllib.parse.urlunparse((parsed.scheme, display_host, parsed.path, "", "", "")) + except (TypeError, ValueError): + return "" + # v0.17: la disk cache di OpenPhish/Spamhaus è stata spostata in # core/intel/cache.py (versionata, con sanity check pre-swap) e il loro # refresh in core/intel/feeds.py (Snapshot condiviso con /api/intel/status @@ -1059,7 +1087,7 @@ def check_url_urlscan(url: str) -> ReputationResult: URLSCAN_API_KEY opzionale per search (ma aumenta il rate limit). """ try: - host = urllib.parse.urlparse(url).netloc.split(":")[0].lstrip("www.") + host = normalize_url_host(url) if not host: return ReputationResult( source="URLScan.io", entity=url, entity_type="url", @@ -1082,7 +1110,7 @@ def check_url_urlscan(url: str) -> ReputationResult: has_api_key = bool(settings.URLSCAN_API_KEY) # Diagnostic logging (v0.14.3+): Log request details (NO sensitive data exposed) - logger.debug(f"[URLScan.io] Processing URL: {url}, host: {host}, is_ip: {is_ip}, query: {query}") + logger.debug("[URLScan.io] Processing host=%s, is_ip=%s", host, is_ip) try: resp = _http_get_with_retry( @@ -1095,7 +1123,7 @@ def check_url_urlscan(url: str) -> ReputationResult: ) if resp.status_code == 401: # Diagnostic logging per 401 error - logger.warning(f"URLScan.io 401 Unauthorized for {url}") + logger.warning("URLScan.io 401 Unauthorized for host=%s", host) if has_api_key: return ReputationResult( source="URLScan.io", entity=url, entity_type="url", @@ -1111,7 +1139,7 @@ def check_url_urlscan(url: str) -> ReputationResult: # Phase 2B: Fallback retry senza API-Key header se HTTP 403 con API key (v0.14.3+) # URLScan.io rifiuta custom sort values senza auth. Rimuovi sort per retry pubblico if resp.status_code == 403 and has_api_key: - logger.debug(f"URLScan.io HTTP 403 con API-Key header, ritentando senza sort param per {url}") + logger.debug("URLScan.io HTTP 403, retry without sort for host=%s", host) resp = _http_get_with_retry( "https://urlscan.io/api/v1/search/", params={ @@ -1124,9 +1152,9 @@ def check_url_urlscan(url: str) -> ReputationResult: rate_key="urlscan", ) if resp.status_code == 200: - logger.debug(f"URLScan.io: retry senza sort riuscito per {url}") + logger.debug("URLScan.io retry without sort succeeded for host=%s", host) else: - logger.debug(f"URLScan.io: retry senza sort fallito (HTTP {resp.status_code}) per {url}") + logger.debug("URLScan.io retry without sort failed (HTTP %s) for host=%s", resp.status_code, host) resp.raise_for_status() data = resp.json() @@ -1169,7 +1197,7 @@ def check_url_urlscan(url: str) -> ReputationResult: except Exception: pass - logger.warning(f"URLScan.io HTTP {code} for {url}. Response: {body}") + logger.warning("URLScan.io HTTP %s for host=%s. Response: %s", code, host, body) # Categorize error and provide service-specific message error_type, error_msg = _categorize_error(e, code, has_api_key=has_api_key) @@ -1191,7 +1219,7 @@ def check_url_urlscan(url: str) -> ReputationResult: except Exception as exc: # Categorize error type error_type, error_msg = _categorize_error(exc) - logger.exception(f"URLScan.io error for {url}: {type(exc).__name__}") + logger.exception("URLScan.io error for host=%s: %s", host, type(exc).__name__) return ReputationResult( source="URLScan.io", entity=url, entity_type="url", error_type=error_type, @@ -1513,41 +1541,73 @@ def check_url_redirect_chain(url: str) -> ReputationResult: Utile per URL shortener (bit.ly, t.co, ecc.) che nascondono la destinazione. """ r = ReputationResult(source="Redirect Chain", entity=url, entity_type="url", queried=True) + response = None try: _rate_limit("redirectchain") - resp = requests.get( - url, - headers={ - "User-Agent": f"Mozilla/5.0 (compatible; EMLyzer/{settings.VERSION})", - "Accept": "text/html,application/xhtml+xml,*/*", - }, - allow_redirects=True, - timeout=REQUEST_TIMEOUT_INFO, - stream=True, # non scarica il body - ) - resp.close() - - chain = [str(r.url) for r in resp.history] + [str(resp.url)] - final = str(resp.url) + current_url = url + previous_scheme = None + chain: list[str] = [] + max_redirects = 5 + + while True: + check_url_safety(current_url, previous_scheme=previous_scheme) + response = requests.get( + current_url, + headers={ + "User-Agent": f"Mozilla/5.0 (compatible; EMLyzer/{settings.VERSION})", + "Accept": "text/html,application/xhtml+xml,*/*", + }, + allow_redirects=False, + timeout=REQUEST_TIMEOUT_INFO, + stream=True, + ) + effective_url = str(response.url or current_url) + chain.append(_redacted_url(effective_url)) + + if not response.is_redirect: + response.close() + response = None + final = effective_url + break + + if len(chain) > max_redirects: + raise FetchError("too_many_redirects", "Troppi redirect") + + location = response.headers.get("Location", "") + if not location: + raise FetchError("malformed_response", "Redirect senza destinazione") + next_url = urllib.parse.urljoin(effective_url, location) + previous_scheme = urllib.parse.urlparse(effective_url).scheme + check_url_safety(next_url, previous_scheme=previous_scheme) + response.close() + response = None + current_url = next_url if len(chain) <= 1: - r.detail = f"Nessun redirect — URL finale: {final}" + r.detail = f"Nessun redirect — URL finale: {_redacted_url(final)}" else: hops = " → ".join(chain) r.detail = f"{len(chain)-1} redirect: {hops[:300]}" # Se la destinazione finale è diversa dall'originale, segnala if final != url: r.confidence = 50.0 # neutro ma merita attenzione + except FetchError as e: + r.error_type = e.category + r.error = e.message except requests.exceptions.SSLError as e: error_type, error_msg = _categorize_error(e) r.error_type = error_type r.error = error_msg except requests.exceptions.ConnectionError as e: - r.error = f"Connessione fallita: {e}" + r.error_type = "connection_error" + r.error = "Connessione fallita" except Exception as e: error_type, error_msg = _categorize_error(e) r.error_type = error_type r.error = error_msg + finally: + if response is not None: + response.close() return r @@ -2187,17 +2247,17 @@ def _s(source, entity, kind, reason): # Redirect chain — solo shortener o HTTP try: parsed = urllib.parse.urlparse(url) - host = parsed.netloc.lower().lstrip("www.") + host = normalize_url_host(url) if host in URL_SHORTENERS or parsed.scheme == "http": _c(check_url_redirect_chain, url, "url") except Exception: pass # crt.sh e CIRCL Passive DNS — solo domini (non IP diretti) try: - host = urllib.parse.urlparse(url).netloc.split(":")[0] + host = normalize_url_host(url) ipaddress.ip_address(host) # se è un IP → ValueError → salta except ValueError: - domain = host.lstrip("www.") + domain = host if domain: _c(check_domain_crtsh, domain, "domain") if settings.CIRCL_API_KEY: @@ -2494,4 +2554,4 @@ def run_slow_checks(ips: list[str], urls: list[str], hashes: list[str], if malicious: avg_conf = sum(r.confidence for r in malicious) / len(malicious) existing.reputation_score = min(avg_conf * 1.2, 100.0) - return existing \ No newline at end of file + return existing diff --git a/backend/nlp_retrain_tabular_model.py b/backend/nlp_retrain_tabular_model.py index 4e9813e..9d38acf 100644 --- a/backend/nlp_retrain_tabular_model.py +++ b/backend/nlp_retrain_tabular_model.py @@ -24,6 +24,7 @@ import csv import json import pickle +import hashlib import logging from pathlib import Path from typing import List, Tuple, Dict @@ -240,6 +241,12 @@ def save_model(self, output_path: str) -> bool: with open(output_path, 'wb') as f: pickle.dump(model_data, f) logger.info(f"Model saved to {output_path}") + model_sha256 = hashlib.sha256(Path(output_path).read_bytes()).hexdigest() + logger.info( + "Model SHA-256: %s (review and update _TABULAR_MODEL_SHA256 in " + "core/analysis/nlp_classifier.py before committing a retrained model)", + model_sha256, + ) return True except Exception as e: logger.error(f"Failed to save model: {e}") diff --git a/backend/nlp_training/nlp_model_tabular_v0.15.1.pkl.hmac b/backend/nlp_training/nlp_model_tabular_v0.15.1.pkl.hmac deleted file mode 100644 index 82cfe5c..0000000 --- a/backend/nlp_training/nlp_model_tabular_v0.15.1.pkl.hmac +++ /dev/null @@ -1 +0,0 @@ -321a3adcf0ae1380d70610e80a9690f434a4bed7f866438f98be4cd4ce23c8b8 \ No newline at end of file diff --git a/backend/tests/test_security_regressions.py b/backend/tests/test_security_regressions.py new file mode 100644 index 0000000..ea15db2 --- /dev/null +++ b/backend/tests/test_security_regressions.py @@ -0,0 +1,137 @@ +"""Regressioni per correzioni di sicurezza e normalizzazione reputazione.""" + +import hashlib +import pickle + +import pytest + +from core.analysis.nlp_classifier import _verify_and_load_pickle +from core.analysis.url_analyzer import _safe_log_host +from core.reputation import connectors +from core.reputation.connectors import normalize_url_host + + +@pytest.mark.parametrize( + ("url", "expected"), + [ + ("https://www.example.com/path", "example.com"), + ("https://web.example.com/path", "web.example.com"), + ("https://user:password@Example.COM:8443/path", "example.com"), + ("https://[2001:db8::1]:443/path", "2001:db8::1"), + ("https://[invalid", ""), + ("not-an-absolute-url", ""), + ], +) +def test_normalized_url_host(url, expected): + assert normalize_url_host(url) == expected + + +def test_pickle_loader_accepts_reviewed_digest(tmp_path): + payload = {"model": "test"} + model_path = tmp_path / "model.pkl" + model_bytes = pickle.dumps(payload) + model_path.write_bytes(model_bytes) + + loaded = _verify_and_load_pickle( + model_path, hashlib.sha256(model_bytes).hexdigest() + ) + + assert loaded == payload + + +def test_pickle_loader_rejects_unreviewed_bytes(tmp_path): + model_path = tmp_path / "model.pkl" + model_path.write_bytes(pickle.dumps({"model": "modified"})) + + with pytest.raises(RuntimeError, match="SHA-256 mismatch"): + _verify_and_load_pickle(model_path, "0" * 64) + + +def test_safe_log_host_strips_sensitive_url_parts(): + assert _safe_log_host("https://user:secret@example.com/path?token=secret#x") == "example.com" + assert _safe_log_host("https://[invalid") == "" + + +@pytest.mark.parametrize( + "url", + [ + "http://127.0.0.1/admin", + "http://10.0.0.1/internal", + "http://169.254.169.254/latest/meta-data/", + ], +) +def test_redirect_chain_blocks_unsafe_initial_targets(url, monkeypatch): + called = False + + def unexpected_request(*args, **kwargs): + nonlocal called + called = True + raise AssertionError("unsafe target must be rejected before the request") + + monkeypatch.setattr(connectors.requests, "get", unexpected_request) + result = connectors.check_url_redirect_chain(url) + + assert called is False + assert result.error_type == "ssrf_blocked" + + +def test_redirect_chain_blocks_public_to_private_redirect(monkeypatch): + class RedirectResponse: + url = "http://93.184.216.34/start" + is_redirect = True + headers = {"Location": "http://127.0.0.1/private"} + closed = False + + def close(self): + self.closed = True + + response = RedirectResponse() + calls = [] + + def fake_get(url, **kwargs): + calls.append((url, kwargs)) + return response + + monkeypatch.setattr(connectors.requests, "get", fake_get) + result = connectors.check_url_redirect_chain("http://93.184.216.34/start") + + assert len(calls) == 1 + assert calls[0][1]["allow_redirects"] is False + assert response.closed is True + assert result.error_type == "ssrf_blocked" + + +def test_redacted_url_hides_credentials_and_tokens(): + assert connectors._redacted_url( + "https://user:secret@example.com:8443/path?token=secret#fragment" + ) == "https://example.com:8443/path" + + +@pytest.mark.asyncio +async def test_urlscan_diagnostic_requires_trusted_client(tmp_path, monkeypatch): + from httpx import ASGITransport, AsyncClient + from api.routes import reputation as reputation_route + from core.intel import auth as intel_auth + from main import app + from utils.config import settings + + monkeypatch.setattr(settings, "DATA_DIR", tmp_path) + monkeypatch.setattr( + reputation_route, + "_test_urlscan_health_sync", + lambda: {"connectivity": True, "api_key_configured": False}, + ) + intel_auth._token_cache = None + token = intel_auth.get_or_create_token() + transport = ASGITransport(app=app, client=("203.0.113.5", 12345)) + + async with AsyncClient(transport=transport, base_url="http://test") as client: + denied = await client.get("/api/reputation/test-urlscan") + allowed = await client.get( + "/api/reputation/test-urlscan", + headers={"X-EMLyzer-Token": token}, + ) + + assert denied.status_code == 403 + assert allowed.status_code == 200 + assert allowed.json()["connectivity"] is True diff --git a/backend/utils/config.py b/backend/utils/config.py index ec10d22..da02c06 100644 --- a/backend/utils/config.py +++ b/backend/utils/config.py @@ -14,7 +14,7 @@ class Settings(BaseSettings): # App APP_NAME: str = "EMLyzer" - VERSION: str = "0.17.2" + VERSION: str = "0.17.3" DEBUG: bool = False # CORS - backend in produzione + Vite dev server @@ -104,4 +104,4 @@ class Settings(BaseSettings): settings.UPLOAD_DIR.mkdir(parents=True, exist_ok=True) settings.REPORTS_DIR.mkdir(parents=True, exist_ok=True) settings.DATA_DIR.mkdir(parents=True, exist_ok=True) -settings.CACHE_DIR.mkdir(parents=True, exist_ok=True) \ No newline at end of file +settings.CACHE_DIR.mkdir(parents=True, exist_ok=True) diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 05a3c6a..f0627a3 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -112,7 +112,7 @@ python3.13 --version **Linux/macOS:** ```bash -tar -xzf EMLyzer_v0.17.2.tar.gz +tar -xzf EMLyzer_v0.17.3.tar.gz cd EMLyzer ``` @@ -139,7 +139,7 @@ A black console window opens showing progress: ``` ============================================ - EMLyzer v0.17.2 + EMLyzer v0.17.3 ============================================ [INFO] Python found: @@ -201,7 +201,7 @@ Open this link in your browser to verify the backend: Expected response: ```json -{"status": "ok", "version": "0.17.2", "app": "EMLyzer"} +{"status": "ok", "version": "0.17.3", "app": "EMLyzer"} ``` --- diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 673dec2..17138ac 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "0.17.2", + "version": "0.17.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "0.17.2", + "version": "0.17.3", "dependencies": { "axios": "^1.20.0", "lucide-react": "^1.48.0", @@ -23,7 +23,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", "globals": "^17.12.0", - "vite": "^8.3.1" + "vite": "^8.3.5" } }, "node_modules/@babel/code-frame": { @@ -511,9 +511,9 @@ "license": "MIT" }, "node_modules/@oxc-project/types": { - "version": "0.151.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", - "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", "dev": true, "license": "MIT", "funding": { @@ -521,9 +521,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", - "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", "cpu": [ "arm" ], @@ -538,9 +538,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", - "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", "cpu": [ "arm64" ], @@ -555,9 +555,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", - "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", "cpu": [ "arm64" ], @@ -572,9 +572,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", - "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", "cpu": [ "x64" ], @@ -589,9 +589,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", - "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", "cpu": [ "x64" ], @@ -606,9 +606,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", - "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", "cpu": [ "arm" ], @@ -623,9 +623,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", - "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", "cpu": [ "arm64" ], @@ -643,9 +643,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", - "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", "cpu": [ "arm64" ], @@ -663,9 +663,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", - "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", "cpu": [ "ppc64" ], @@ -683,9 +683,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", - "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", "cpu": [ "s390x" ], @@ -703,9 +703,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", - "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", "cpu": [ "x64" ], @@ -723,9 +723,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", - "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", "cpu": [ "x64" ], @@ -743,9 +743,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", - "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", "cpu": [ "arm64" ], @@ -760,9 +760,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", - "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", "cpu": [ "arm64" ], @@ -777,9 +777,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", - "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", "cpu": [ "x64" ], @@ -1468,7 +1468,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.16.0", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", "funding": [ { "type": "individual", @@ -2131,9 +2133,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -2243,9 +2245,9 @@ } }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", "dev": true, "funding": [ { @@ -2263,9 +2265,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -2376,13 +2378,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", - "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.151.0", + "@oxc-project/types": "=0.155.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -2392,21 +2394,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.11", - "@rolldown/binding-android-arm64": "1.2.11", - "@rolldown/binding-darwin-arm64": "1.2.11", - "@rolldown/binding-darwin-x64": "1.2.11", - "@rolldown/binding-freebsd-x64": "1.2.11", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", - "@rolldown/binding-linux-arm64-gnu": "1.2.11", - "@rolldown/binding-linux-arm64-musl": "1.2.11", - "@rolldown/binding-linux-ppc64-gnu": "1.2.11", - "@rolldown/binding-linux-s390x-gnu": "1.2.11", - "@rolldown/binding-linux-x64-gnu": "1.2.11", - "@rolldown/binding-linux-x64-musl": "1.2.11", - "@rolldown/binding-openharmony-arm64": "1.2.11", - "@rolldown/binding-win32-arm64-msvc": "1.2.11", - "@rolldown/binding-win32-x64-msvc": "1.2.11" + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" } }, "node_modules/scheduler": { @@ -2449,9 +2451,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2526,16 +2528,16 @@ } }, "node_modules/vite": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", - "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.9", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "bin": { @@ -2552,7 +2554,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -2642,7 +2644,9 @@ } }, "node_modules/zod": { - "version": "4.3.6", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "dev": true, "license": "MIT", "funding": { diff --git a/frontend/package.json b/frontend/package.json index 504e175..366bcfe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.17.2", + "version": "0.17.3", "type": "module", "scripts": { "dev": "vite", @@ -17,7 +17,7 @@ "react-router-dom": "^7.18.4" }, "overrides": { - "follow-redirects": "1.16.0", + "follow-redirects": "1.16.1", "@babel/core": "7.29.6" }, "devDependencies": { @@ -29,6 +29,6 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", "globals": "^17.12.0", - "vite": "^8.3.1" + "vite": "^8.3.5" } }