From a8b1e88f1113cef3a139c10ce767239052c0ac8e Mon Sep 17 00:00:00 2001 From: sck_0 Date: Sun, 15 Mar 2026 08:40:53 +0100 Subject: [PATCH] fix: restore auth and transport integrity defaults --- .../__tests__/useSkillStarsSecurity.test.ts | 42 +++++++++++++++++++ apps/web-app/src/hooks/useSkillStars.ts | 4 +- apps/web-app/src/lib/supabase.ts | 4 ++ .../scripts/scraper/base_scraper.py | 25 +++++++---- .../scripts/web_scraper_fallback.py | 6 ++- .../scripts/tests/test_junta_tls_security.py | 41 ++++++++++++++++++ 6 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 apps/web-app/src/hooks/__tests__/useSkillStarsSecurity.test.ts create mode 100644 tools/scripts/tests/test_junta_tls_security.py diff --git a/apps/web-app/src/hooks/__tests__/useSkillStarsSecurity.test.ts b/apps/web-app/src/hooks/__tests__/useSkillStarsSecurity.test.ts new file mode 100644 index 00000000..2bae1722 --- /dev/null +++ b/apps/web-app/src/hooks/__tests__/useSkillStarsSecurity.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { renderHook, act, waitFor } from '@testing-library/react'; + +const maybeSingle = vi.fn().mockResolvedValue({ data: null, error: null }); +const upsert = vi.fn().mockResolvedValue({ error: null }); +const select = vi.fn(() => ({ eq: vi.fn(() => ({ maybeSingle })) })); +const from = vi.fn(() => ({ select, upsert })); + +vi.mock('../../lib/supabase', () => ({ + supabase: { + from, + }, + sharedStarWritesEnabled: false, +})); + +describe('useSkillStars shared writes', () => { + beforeEach(() => { + localStorage.clear(); + vi.clearAllMocks(); + from.mockReturnValue({ select, upsert }); + select.mockReturnValue({ eq: vi.fn(() => ({ maybeSingle })) }); + maybeSingle.mockResolvedValue({ data: null, error: null }); + upsert.mockResolvedValue({ error: null }); + }); + + it('does not upsert shared star counts when frontend writes are disabled', async () => { + const { useSkillStars } = await import('../useSkillStars'); + const { result } = renderHook(() => useSkillStars('shared-stars-disabled')); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + await act(async () => { + await result.current.handleStarClick(); + }); + + expect(upsert).not.toHaveBeenCalled(); + expect(result.current.hasStarred).toBe(true); + expect(result.current.starCount).toBe(1); + }); +}); diff --git a/apps/web-app/src/hooks/useSkillStars.ts b/apps/web-app/src/hooks/useSkillStars.ts index 7583cbf8..ee4f92ec 100644 --- a/apps/web-app/src/hooks/useSkillStars.ts +++ b/apps/web-app/src/hooks/useSkillStars.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { supabase } from '../lib/supabase'; +import { sharedStarWritesEnabled, supabase } from '../lib/supabase'; const STORAGE_KEY = 'user_stars'; @@ -102,7 +102,7 @@ export function useSkillStars(skillId: string | undefined): UseSkillStarsReturn saveUserStarsToStorage(updatedStars); // Sync to Supabase if available - if (supabase) { + if (supabase && sharedStarWritesEnabled) { try { // Fetch current count first const { data: current } = await supabase diff --git a/apps/web-app/src/lib/supabase.ts b/apps/web-app/src/lib/supabase.ts index c4a39b2a..ce5fb749 100644 --- a/apps/web-app/src/lib/supabase.ts +++ b/apps/web-app/src/lib/supabase.ts @@ -11,6 +11,10 @@ const supabaseAnonKey = (import.meta as ImportMeta & { env: Record }).env.VITE_SUPABASE_ANON_KEY || 'sb_publishable_CyVwHGbtT80AuDFmXNkc9Q_YNcamTGg' +export const sharedStarWritesEnabled = + ((import.meta as ImportMeta & { env: Record }).env.VITE_ENABLE_SHARED_STAR_WRITES ?? '') + .toLowerCase() === 'true' + // Create a single supabase client for interacting with the database export const supabase: SupabaseClient = createClient(supabaseUrl, supabaseAnonKey) diff --git a/skills/junta-leiloeiros/scripts/scraper/base_scraper.py b/skills/junta-leiloeiros/scripts/scraper/base_scraper.py index 61663eb9..af93a20f 100644 --- a/skills/junta-leiloeiros/scripts/scraper/base_scraper.py +++ b/skills/junta-leiloeiros/scripts/scraper/base_scraper.py @@ -7,17 +7,19 @@ from __future__ import annotations import asyncio import logging +import os from abc import ABC, abstractmethod from dataclasses import dataclass, field from datetime import datetime, timezone -from typing import List, Optional - -import httpx -from bs4 import BeautifulSoup +from typing import Any, List, Optional logger = logging.getLogger(__name__) +def should_verify_tls() -> bool: + return os.getenv("JUNTA_INSECURE_TLS", "").lower() not in {"1", "true", "yes", "on"} + + @dataclass class Leiloeiro: estado: str @@ -80,16 +82,20 @@ class AbstractJuntaScraper(ABC): params: Optional[dict] = None, data: Optional[dict] = None, method: str = "GET", - ) -> Optional[BeautifulSoup]: + ) -> Optional[Any]: """Faz o request HTTP com retry e retorna BeautifulSoup ou None.""" + import httpx + from bs4 import BeautifulSoup + target = url or self.url + verify_tls = should_verify_tls() for attempt in range(1, self.max_retries + 1): try: async with httpx.AsyncClient( headers=self.HEADERS, timeout=self.timeout, follow_redirects=True, - verify=False, # alguns sites gov têm cert self-signed + verify=verify_tls, ) as client: if method.upper() == "POST": resp = await client.post(target, data=data, params=params) @@ -173,9 +179,12 @@ class AbstractJuntaScraper(ABC): url: Optional[str] = None, wait_selector: Optional[str] = None, wait_ms: int = 3000, - ) -> Optional[BeautifulSoup]: + ) -> Optional[Any]: """Renderiza página com JavaScript usando Playwright. Retorna BeautifulSoup ou None.""" + from bs4 import BeautifulSoup + target = url or self.url + verify_tls = should_verify_tls() try: from playwright.async_api import async_playwright except ImportError: @@ -188,7 +197,7 @@ class AbstractJuntaScraper(ABC): ctx = await browser.new_context( user_agent=self.HEADERS["User-Agent"], locale="pt-BR", - ignore_https_errors=True, + ignore_https_errors=not verify_tls, ) page = await ctx.new_page() await page.goto(target, timeout=60000, wait_until="networkidle") diff --git a/skills/junta-leiloeiros/scripts/web_scraper_fallback.py b/skills/junta-leiloeiros/scripts/web_scraper_fallback.py index e85aa908..679f91cf 100644 --- a/skills/junta-leiloeiros/scripts/web_scraper_fallback.py +++ b/skills/junta-leiloeiros/scripts/web_scraper_fallback.py @@ -24,6 +24,7 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from db import Database +from scraper.base_scraper import should_verify_tls from scraper.states import SCRAPERS logger = logging.getLogger(__name__) @@ -114,7 +115,10 @@ async def _direct_extract(estado: str, url: str) -> list[dict]: results = [] try: async with httpx.AsyncClient( - headers=headers, timeout=30.0, follow_redirects=True, verify=False + headers=headers, + timeout=30.0, + follow_redirects=True, + verify=should_verify_tls(), ) as client: resp = await client.get(url) if resp.status_code >= 400: diff --git a/tools/scripts/tests/test_junta_tls_security.py b/tools/scripts/tests/test_junta_tls_security.py new file mode 100644 index 00000000..fd3e3e36 --- /dev/null +++ b/tools/scripts/tests/test_junta_tls_security.py @@ -0,0 +1,41 @@ +import importlib.util +import os +import sys +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +def load_module(relative_path: str, module_name: str): + module_path = REPO_ROOT / relative_path + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +base_scraper = load_module( + "skills/junta-leiloeiros/scripts/scraper/base_scraper.py", + "junta_base_scraper", +) + + +class JuntaTlsSecurityTests(unittest.TestCase): + def test_tls_verification_is_enabled_by_default(self): + os.environ.pop("JUNTA_INSECURE_TLS", None) + self.assertTrue(base_scraper.should_verify_tls()) + + def test_tls_verification_can_be_disabled_explicitly(self): + os.environ["JUNTA_INSECURE_TLS"] = "1" + try: + self.assertFalse(base_scraper.should_verify_tls()) + finally: + os.environ.pop("JUNTA_INSECURE_TLS", None) + + +if __name__ == "__main__": + unittest.main()