File size: 4,586 Bytes
afd56bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import re
import logging
from typing import Dict

logger = logging.getLogger(__name__)


class SensitiveDataGuard:
    """
    Rygorystyczny mechanizm hybrydowy (RegEx + opcjonalnie LLM) do ukrywania PII
    oraz tajemnicy przedsiębiorstwa (know-how, telemetria, finanse).

    Warstwy:
      1. RegEx — NIP, PESEL, KRS, IBAN, Email, Telefon, Imię+Nazwisko (heurystyka)
      2. LLM (Bielik/Gemini) — semantyczne maskowanie know-how (opcjonalnie)

    Zgodność: RODO Art. 4, AI Act Art. 10 (data governance for high-risk AI).
    """

    def __init__(self):
        self.mapping: Dict[str, str] = {}
        self.counter = 1

        # ── Wzorce PII ──────────────────────────────────────────────────────
        self.patterns = {
            # Identyfikatory biznesowe
            "NIP": r"\b\d{3}[- ]?\d{3}[- ]?\d{2}[- ]?\d{2}\b",
            "PESEL": r"\b\d{11}\b",
            "KRS": r"\b(?:KRS[:\s]*)?\d{10}\b",
            "REGON": r"\b\d{9}(?:\d{5})?\b",
            # Dane kontaktowe
            "EMAIL": r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b",
            "TELEFON": r"(?:\+48\s?)?(?:\d{2,3}[- ]?){3,4}\d{2,3}",
            # Bankowe
            "IBAN": r"\bPL\d{2}[ ]?\d{4}[ ]?\d{4}[ ]?\d{4}[ ]?\d{4}[ ]?\d{4}[ ]?\d{4}\b",
            # Patenty / Finanse
            "PATENT": r"\b(?:Pat\.|Zgłoszenie P\.)\s*\d+\b",
            "FINANSE": r"\b\d{1,3}(?:[ .,]\d{3})*(?:,\d{2})?\s*(?:PLN|EUR|USD|zł)\b",
            # Imię + Nazwisko (heurystyka: 2 słowa z wielkich liter, pl-locale)
            "OSOBA": r"\b[A-ZŁŚŻŹĆĄÓĘŃ][a-złśżźćąóęń]{2,}\s+[A-ZŁŚŻŹĆĄÓĘŃ][a-złśżźćąóęń]{2,}\b",
            # Adresy
            "ADRES": r"\bul\.\s+[A-ZŁŚŻŹĆĄÓĘŃ][^\n,]{3,40},\s*\d{2}-\d{3}\s+[A-ZŁŚŻŹĆĄÓĘŃ][^\n,]{3,30}\b",
        }

    def anonymize_text(self, text: str) -> str:
        """
        Zastępuje wrażliwe fragmenty na tokeny.
        Warstwa 1 (RegEx) + Warstwa 2 (LLM Bielik).
        """
        if not text:
            return text

        anonymized = text
        for pii_type, pattern in self.patterns.items():
            matches = set(re.findall(pattern, anonymized, flags=re.IGNORECASE))
            for match in matches:
                if match not in self.mapping:
                    token = f"<{pii_type}_{self.counter}>"
                    self.mapping[match] = token
                    self.counter += 1
                anonymized = anonymized.replace(match, self.mapping[match])

        # Warstwa 2: LLM (Bielik) do semantycznego maskowania Know-How
        try:
            import sys
            import os

            sys.path.append(os.path.join(os.path.dirname(__file__), "..", ".."))
            from backend.core.llm_router import get_llm
            from langchain_core.messages import SystemMessage, HumanMessage

            llm = get_llm(task_type="pii_anonymization")
            prompt = (
                "Jesteś surowym strażnikiem RODO i tajemnic przedsiębiorstwa. "
                "Twoim zadaniem jest zamiana WŁAŚCIWYCH NAZW unikalnych technologii, algorytmów "
                "i autorskich rozwiązań know-how na tag <KNOW_HOW_X>.\n"
                "MUSISZ zachować całą resztę tekstu IDEALNIE nienaruszoną (co do znaku). "
                "Nie modyfikuj zdań, nie tłumacz, tylko podmień wybrane słowa."
            )
            resp = llm.invoke(
                [SystemMessage(content=prompt), HumanMessage(content=anonymized)]
            )
            if resp and resp.content and "<KNOW_HOW_" in resp.content.upper():
                anonymized = resp.content
        except Exception as e:
            logger.warning(
                "Ollama z modelem Bielik jest niedostępna. De-identyfikacja odbywa się tylko na warstwie RegEx. Analizy prawne mogą być mniej precyzyjne."
            )
            logger.debug(f"Szczegóły błędu: {e}")

        return anonymized

    def deanonymize_text(self, text: str) -> str:
        """Przywraca tokeny do pierwotnej postaci w odpowiedzi"""
        if not text:
            return text

        deanonymized = text
        reverse_mapping = {v: k for k, v in self.mapping.items()}
        for token, original_value in reverse_mapping.items():
            deanonymized = deanonymized.replace(token, original_value)

        return deanonymized

    def reset_for_session(self):
        self.mapping = {}
        self.counter = 1


anonymizer = SensitiveDataGuard()